Pod,是 Kubernetes 项目中最小的 API 对象。如果换一个更专业的说法,我们可以这样描述:Pod,是 Kubernetes 项目的原子调度单位。

不难发现,在一个真正的操作系统里,进程并不是“孤苦伶仃”地独自运行的,而是以进程组的方式,“有原则地”组织在一起。比如,这里有一个叫作 rsyslogd 的程序,它负责的是 Linux 操作系统里的日志处理。可以看到,rsyslogd 的主程序 main,和它要用到的内核日志模块 imklog 等,同属于 1632 进程组。这些进程相互协作,共同完成 rsyslogd 程序的职责。

而 Kubernetes 项目所做的,其实就是将“进程组”的概念映射到了容器技术中,并使其成为了这个云计算“操作系统”里的“一等公民”。

Kubernetes 项目之所以要这么做的原因,我在前面介绍 Kubernetes 和 Borg 的关系时曾经提到过:在 Borg 项目的开发和实践过程中,Google 公司的工程师们发现,他们部署的应用,往往都存在着类似于“进程和进程组”的关系。更具体地说,就是这些应用之间有着密切的协作关系,使得它们必须部署在同一台机器上。

Pod 是 Kubernetes 里的原子调度单位。这就意味着,Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。pod是容器组

所以,像 imklog、imuxsock 和 main 函数主进程这样的三个容器,正是一个典型的由三个容器组成的 Pod。Kubernetes 项目在调度时,自然就会去选择可用内存等于 3 GB 的 node-1 节点进行绑定,而根本不会考虑 node-2。

像这样容器间的紧密协作,我们可以称为“超亲密关系”。这些具有“超亲密关系”容器的典型特征包括但不限于:互相之间会发生直接的文件交换、使用 localhost 或者 Socket 文件进行本地通信、会发生非常频繁的远程调用、需要共享某些 Linux Namespace(比如,一个容器要加入另一个容器的 Network Namespace)等等。

Pod 在 Kubernetes 项目里还有更重要的意义,那就是:容器设计模式。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux

所以,在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。这样的组织关系,可以用下面这样一个示意图来表达:

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_02

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_03

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_04

但不要忘记,Pod 的另一个重要特性是,它的所有容器都共享同一个 Network Namespace。这就使得很多与 Pod 网络相关的配置和管理,也都可以交给 sidecar 完成,而完全无须干涉用户容器。这里最典型的例子莫过于 Istio 这个微服务治理项目了。

深入解析Pod对象 基本概念

Pod,而不是容器,才是 Kubernetes 项目中的最小编排单位,容器(Container)就成了 Pod 属性里的一个普通的字段

而如果你能把 Pod 看成传统环境里的“机器”、把容器看作是运行在这个“机器”里的“用户程序”,那么很多关于 Pod 对象的设计就非常容易理解了。

凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。

凡是跟容器的 Linux Namespace 相关的属性,也一定是 Pod 级别的。这个原因也很容易理解:Pod 的设计,就是要让它里面的容器尽可能多地共享 Linux Namespace,仅保留必要的隔离和限制能力。这样,Pod 模拟出的效果,就跟虚拟机里程序间的关系非常类似了

凡是 Pod 中的容器要共享宿主机的 Namespace,也一定是 Pod 级别的定义

首先,是 ImagePullPolicy 字段。它定义了镜像拉取的策略。而它之所以是一个 Container 级别的属性,是因为容器镜像本来就是 Container 定义中的一部分。

ImagePullPolicy 的值默认是 Always,即每次创建 Pod 都重新拉取一次镜像。另外,当容器的镜像是类似于 nginx 或者 nginx:latest 这样的名字时,ImagePullPolicy 也会被认为 Always。

其次,是 Lifecycle 字段。它定义的是 Container Lifecycle Hooks。顾名思义,Container Lifecycle Hooks 的作用,是在容器状态发生变化时触发一系列“钩子”

先说 postStart 吧。它指的是,在容器启动后,立刻执行一个指定的操作。需要明确的是,postStart 定义的操作,虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。

而类似地,preStop 发生的时机,则是容器被杀死之前(比如,收到了 SIGKILL 信号)。而需要明确的是,preStop 操作的执行,是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样。

pod的状态

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_05

在 Kubernetes 中,有几种特殊的 Volume,它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用,是为容器提供预先定义好的数据。所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。这正是 Projected Volume 的含义。

到目前为止,Kubernetes 支持的 Projected Volume 一共有四种:

Secret : 帮你把 Pod 想要访问的加密数据,存放到 Etcd 中,典型是存放数据库的Credential信息。直接通过编写 YAML 文件的方式来创建这个 Secret 对象

apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
user: YWRtaW4=
pass: MWYyZDFlMmU2N2Rm

ConfigMap :和Secret 的区别在于,ConfigMap 保存的是不需要加密的、应用所需的配置信息;可以使用 kubectl create configmap 从文件或者目录创建 ConfigMap,也可以直接编写 ConfigMap 对象的 YAML 文件。

Downward API :让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。比如声明了要暴露 Pod 的 metadata.labels 信息给容器

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_06

  其实,Secret、ConfigMap,以及 Downward API 这三种 Projected Volume 定义的信息,大多还可以通过环境变量的方式出现在容器里。但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,我都建议你使用 Volume 文件的方式获取这些信息。

ServiceAccountToken:是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。比如,Service Account A,可以只被允许对 Kubernetes API 进行 GET 操作,而 Service Account B,则可以有 Kubernetes API 的所有操作权限。  

像这样的 Service Account 的授权信息和文件,实际上保存在它所绑定的一个特殊的 Secret 对象里的。这个特殊的 Secret 对象,就叫作 ServiceAccountToken

 这种把 Kubernetes 客户端以容器的方式运行在集群里,然后使用 default Service Account 自动授权的方式,被称作“InClusterConfig”,也是我最推荐的进行 Kubernetes API 编程的授权方式。

  在 Kubernetes 中,你可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。

而作为用户,你还可以通过设置 restartPolicy,改变 Pod 的恢复策略。除了 Always,它还有 OnFailure 和 Never 两种情况:

Always:在任何情况下,只要容器不在运行状态,就自动重启容器;

OnFailure: 只在容器 异常时才自动重启容器;

Never: 从来不重启容器。

++++++++++++++++++++++++++++++++++++++++

编排控制器模型

Pod 这个看似复杂的 API 对象,实际上就是对容器的进一步抽象和封装而已。

Pod 对象,其实就是容器的升级版。它对容器进行了组合,添加了更多的属性和字段。这就好比给集装箱四面安装了吊环,使得 Kubernetes 这架“吊车”,可以更轻松地操作它。

而 Kubernetes 操作这些“集装箱”的逻辑,都由控制器(Controller)完成

kube-controller-manager :一系列控制器的集合

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_07

for {
实际状态 := 获取集群中对象X的实际状态(Actual State)
期望状态 := 获取集群中对象X的期望状态(Desired State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}

  kubelet 通过心跳汇报的容器状态和节点状态,或者监控系统中保存的应用监控数据,或者控制器主动收集的它自己感兴趣的信息,这些都是常见的实际状态的来源。

而期望状态,一般来自于用户提交的 YAML 文件。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_08

增加 Pod,删除已有的 Pod,或者更新 Pod 的某个字段。这也是 Kubernetes 项目“面向 API 对象编程”的一个直观体现。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_09

作业副本与水平扩展

一个 ReplicaSet 对象,其实就是由副本数目的定义和一个 Pod 模板组成的。不难发现,它的定义其实是 Deployment 的一个子集。

更重要的是,Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_10

“水平扩展 / 收缩”非常容易实现,Deployment Controller 只需要修改它所控制的 ReplicaSet 的 Pod 副本个数就可以了。

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_11

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_12

像这样,将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_13

V1 到 V2版本升级,采用了滚动更新

想回滚到更早之前的版本 : 

使用 kubectl rollout history 命令,查看每次 Deployment 变更对应的版本

然后,我们就可以在 kubectl rollout undo 命令行最后,加上要回滚到的指定版本的版本号,就可以回滚到指定版本了

Kubernetes 项目还提供了一个指令,使得我们对 Deployment 的多次更新操作,最后 只生成一个 ReplicaSet。

深入理解StatefulSet

拓扑状态

这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)。

Kubernetes 项目很早就在 Deployment 的基础上,扩展出了对“有状态应用”的初步支持。这个编排功能,就是:StatefulSet。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_14

Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod。

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_15

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_16

nginx字段,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。

$ kubectl get pods -w -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 19s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 20s

StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是:<statefulset name>-<ordinal index>。

而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。

在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_17

Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。

存储状态

Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)和 Persistent Volume(PV)的 API 对象,大大降低了用户声明和使用持久化 Volume 的门槛。

第一步:定义一个 PVC,声明想要的 Volume 的属性

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

第二步:在应用的 Pod 中,声明使用这个 PVC:

apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim

  所以,Kubernetes 中 PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想。开发者只要知道并会使用“接口”,即:PVC;而运维人员则负责给“接口”绑定具体的实现,即:PV。

如果你还是不太理解 PVC 的话,可以先记住这样一个结论:PVC 其实就是一种特殊的 Volume。只不过一个 PVC 具体是什么类型的 Volume,要在跟某个 PV 绑定之后才知道。

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_18

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_19

有状态应用实践

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_20

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_21

第一步 : 通过 XtraBackup 将 Master 节点的数据备份到指定目录。

第二步:配置 Slave 节点

第三步: 启动 Slave 节点

第四步 :在这个集群中添加更多的 Slave 节点

Pod 的启动过程

第一步:从 ConfigMap 中,获取 MySQL 的 Pod 对应的配置文件。

第二步:在 Slave Pod 启动前,从 Master 或者其他 Slave Pod 里拷贝数据库数据到自己的目录下。

当初始化所需的数据(/var/lib/mysql/mysql 目录)已经存在,或者当前 Pod 是 Master 节点的时候,不需要做拷贝操作。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_22

在完成 MySQL 节点的初始化后,这个 sidecar 容器的第二个工作,则是启动一个数据传输服务。

首先,我们需要在 Kubernetes 集群里创建满足条件的 PV

然后,我们就可以创建这个 StatefulSet 了

接下来,我们可以尝试向这个 MySQL 集群发起请求,执行一些 SQL 操作来验证它是否正常


守护进程 DaemonSet

StatefulSet Controller 就会按照与 Pod 编号相反的顺序,从最后一个 Pod 开始,逐一更新这个 StatefulSet 管理的每个 Pod。而如果更新发生了错误,这次“滚动更新”就会停止。此外,StatefulSet 的“滚动更新”还允许我们进行更精细的控制,比如金丝雀发布(Canary Deploy)或者灰度发布,这意味着应用的多个实例中被指定的一部分不会被更新到最新的版本。

DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 Daemon Pod。 所以,这个 Pod 有如下三个特征:

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_23

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_24

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_25

接下来,我们来把这个 DaemonSet 的容器镜像版本到 v2.2.0

Job与CronJob

之前主要编排的对象,都是“在线业务”,即:Long Running Task(长作业)。比如,我在前面举例时常用的 Nginx、Tomcat,以及 MySQL 等等。这些应用一旦运行起来,除非出错或者停止,它的容器进程会一直保持在 Running 状态

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_26

这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_27

Job Controller 重新创建 Pod 的间隔是呈指数增加的,即下一次重新创建 Pod 的动作会分别发生在 10 s、20 s、40 s …后。

Job Controller 实际上控制了,作业执行的并行度,以及总共需要完成的任务数这两个重要参数。而在实际使用时,你需要根据作业的特性,来决定并行度(parallelism)和任务数(completions)的合理取值。

CronJob 与 Job 的关系,正如同 Deployment 与 ReplicaSet 的关系一样。CronJob 是一个专门用来管理 Job 对象的控制器。只不过,它创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。

比如,"*/1 * * * *"。

声明式API与Kubernetes编程范式

这个 YAML 文件,正是 Kubernetes 声明式 API 所必须具备的一个要素

Docker Swarm 的编排操作都是基于命令行的

$ docker service create --name nginx --replicas 2  nginx
$ docker service update --image nginx:1.7.9 nginx

像这样的两条命令,就是用 Docker Swarm 启动了两个 Nginx 容器实例。其中,第一条 create 命令创建了这两个容器,而第二条 update 命令则把它们“滚动更新”成了一个新的镜像。

对于这种使用方式,我们称为命令式命令行操作

在 Kubernetes 里又该怎么做呢?

我们需要在本地编写一个 Deployment 的 YAML 文件:

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80

我们还需要使用 kubectl create 命令在 Kubernetes 里创建这个 Deployment 对象:

我们还需要使用 kubectl create 命令在 Kubernetes 里创建这个 Deployment 对象:

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_28

对于上面这种先 kubectl create,再 replace 的操作,我们称为命令式配置文件操作。

到底什么才是“声明式 API”呢?

使用 kubectl apply 命令来创建这个 Deployment
kubectl apply -f nginx.yaml
继续执行一条 kubectl apply 命令,即:
$ kubectl apply -f nginx.yaml

kubectl replace 的执行过程,是使用新的 YAML 文件中的 API 对象,替换原有的 API 对象;而 kubectl apply,则是执行了一个对原有 API 对象的 PATCH 操作。

而 Istio 项目,实际上就是一个基于 Kubernetes 项目的微服务治理框架。它的架构非常清晰,如下所示:

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_29

Istio 项目使用的,是 Kubernetes 中的一个非常重要的功能,叫作 Dynamic Admission Control。

Kubernetes 项目为我们额外提供了一种“热插拔”式的 Admission 机制,它就是 Dynamic Admission Control,也叫作:Initializer。

Istio 要做的,就是编写一个用来为 Pod“自动注入”Envoy 容器的 Initializer。

首先,Istio 会将这个 Envoy 容器本身的定义,以 ConfigMap 的方式保存在 Kubernetes 当中。

接下来,Istio 将一个编写好的 Initializer,作为一个 Pod 部署在 Kubernetes 中,配置如下

apiVersion: v1
kind: Pod
metadata:
labels:
app: envoy-initializer
name: envoy-initializer
spec:
containers:
- name: envoy-initializer
image: envoy-initializer:0.0.1
imagePullPolicy: Always


Kubernetes 的 API 库,为我们提供了一个方法,使得我们可以直接使用新旧两个 Pod 对象,生成一个 TwoWayMergePatch:

有了这个 TwoWayMergePatch 之后,Initializer 的代码就可以使用这个 patch 的数据,调用 Kubernetes 的 Client,发起一个 PATCH 请求。

这也就意味着,当你在 Initializer 里完成了要做的操作后,一定要记得将这个 metadata.initializers.pending 标志清除掉。这一点,你在编写 Initializer 代码的时候一定要非常注意。

Istio 项目的核心,就是由无数个运行在应用 Pod 中的 Envoy 容器组成的服务代理网格。这也正是 Service Mesh 的含义。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_30

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_31

在 Kubernetes 项目中,一个 API 对象在 Etcd 里的完整资源路径,是由:Group(API 组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成的。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_32

 Kubernetes 里 API 对象的组织方式,其实是层层递进的

首先,Kubernetes 会匹配 API 对象的组。

然后,Kubernetes 会进一步匹配到 API 对象的版本号。

最后,Kubernetes 会匹配 API 对象的资源类型。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_33

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_34

编写自定义控制器

基于声明式 API 的业务功能实现,往往需要通过控制器模式来“监视”API 对象的变化(比如,创建或者删除 Network),然后以此来决定实际要执行的具体工作

编写自定义控制器代码的过程包括:编写 main 函数、编写自定义控制器的定义,以及编写控制器里的业务逻辑三个部分。

main 函数的主要工作就是,定义并初始化一个自定义控制器(Custom Controller),然后启动它

func main() {
...

cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
...
kubeClient, err := kubernetes.NewForConfig(cfg)
...
networkClient, err := clientset.NewForConfig(cfg)
...

networkInformerFactory := informers.NewSharedInformerFactory(networkClient, ...)

controller := NewController(kubeClient, networkClient,
networkInformerFactory.Samplecrd().V1().Networks())

go networkInformerFactory.Start(stopCh)

if err = controller.Run(2, stopCh); err != nil {
glog.Fatalf("Error running controller: %s", err.Error())
}
}

第一步:main 函数根据我提供的 Master 配置(APIServer 的地址端口和 kubeconfig 的路径),创建一个 Kubernetes 的 client(kubeClient)和 Network 对象的 client(networkClient)。

第二步:main 函数为 Network 对象创建一个叫作 InformerFactory(即:networkInformerFactory)的工厂,并使用它生成一个 Network 对象的 Informer,传递给控制器。

第三步:main 函数启动上述的 Informer,然后执行 controller.Run,启动自定义控制器

在 Kubernetes 项目中,一个自定义控制器的工作原理,可以用下面这样一幅流程图来表示

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_35

这个控制器要做的第一件事,是从 Kubernetes 的 APIServer 里获取它所关心的对象,也就是我定义的 Network 对象。

比如,如果事件类型是 Added(添加对象),那么 Informer 就会通过一个叫作 Indexer 的库把这个增量里的 API 对象保存在本地缓存中,并为它创建索引。相反,如果增量的事件类型是 Deleted(删除对象),那么 Informer 就会从本地缓存中删除这个对象。

这个同步本地缓存的工作,是 Informer 的第一个职责,也是它最重要的职责。

Informer 的第二个职责,则是根据这些事件的类型,触发事先注册好的 ResourceEventHandler。

我前面在 main 函数里创建了两个 client(kubeclientset 和 networkclientset),然后在这段代码里,使用这两个 client 和前面创建的 Informer,初始化了自定义控制器。

然后,我为 networkInformer 注册了三个 Handler(AddFunc、UpdateFunc 和 DeleteFunc),分别对应 API 对象的“添加”“更新”和“删除”事件。而具体的处理操作,都是将该事件对应的 API 对象加入到工作队列中。

所谓 Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。它是自定义控制器跟 APIServer 进行数据同步的重要组件。

需要注意的是,这个定时 resync 操作,也会触发 Informer 注册的“更新”事件。但此时,这个“更新”事件对应的 Network 对象实际上并没有发生变化,即:新、旧两个 Network 对象的 ResourceVersion 是一样的。在这种情况下,Informer 就不需要对这个更新事件再做进一步的处理了。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_36

Reflector 和 Informer 之间,用到了一个“增量先进先出队列”进行协同。而 Informer 与你要编写的控制循环之间,则使用了一个工作队列来进行协同。

所以,接下来,作为开发者,你就只需要关注如何拿到“实际状态”,然后如何拿它去跟“期望状态”做对比,从而决定接下来要做的业务逻辑即可。

基于角色的权限控制:RBAC

在 Kubernetes 项目中,负责完成授权(Authorization)工作的机制,就是 RBAC:基于角色的访问控制(Role-Based Access Control)。

Kubernetes 学习02  容器编排与Kubernetes作业管理_linux_37

Role 本身就是一个 Kubernetes 的 API 对象

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: mynamespace
name: example-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]

RoleBinding 本身也是一个 Kubernetes 的 API 对象

这个 RoleBinding 对象里定义了一个 subjects 字段,即“被作用者”。它的类型是 User

接下来,我们会看到一个 roleRef 字段。正是通过这个字段,RoleBinding 对象就可以直接通过名字,来引用我们前面定义的 Role 对象(example-role),从而定义了“被作用者(Subject)”和“角色(Role)”之间的绑定关系。

Role 和 RoleBinding 对象都是 Namespaced 对象(Namespaced Object),它们对权限的限制规则仅在它们自己的 Namespace 内有效,roleRef 也只能引用当前 Namespace 里的 Role 对象。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_38

+++++++++++++++++++++++++++++++++++++++++++++

Operator工作原理解读

在 Kubernetes 中,管理“有状态应用”是一个比较复杂的过程,尤其是编写 Pod 模板的时候,总有一种“在 YAML 文件里编程序”的感觉,让人很不舒服。

而在 Kubernetes 生态中,还有一个相对更加灵活和编程友好的管理“有状态应用”的解决方案,它就是:Operator

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_39

而 Etcd Operator 本身,其实就是一个 Deployment,它的 YAML 文件如下所示


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: etcd-operator
spec:
replicas: 1
template:
metadata:
labels:
name: etcd-operator
spec:
containers:
- name: etcd-operator
image: quay.io/coreos/etcd-operator:v0.9.2
command:
- etcd-operator
env:
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
...

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_40

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_41

Operator 的工作原理,实际上是利用了 Kubernetes 的自定义 API 资源(CRD),来描述我们想要部署的“有状态应用”;然后在自定义控制器里,根据自定义 API 对象的变化,来完成具体的部署和运维工作。

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_42

这些节点启动参数里的–initial-cluster 参数,非常值得你关注。它的含义,正是当前节点启动时集群的拓扑结构。说得更详细一点,就是当前这个节点启动时,需要跟哪些节点通信来组成集群

Kubernetes 学习02  容器编排与Kubernetes作业管理_ide_43

首先,Etcd Operator 会创建一个“种子节点”;

然后,Etcd Operator 会不断创建新的 Etcd 节点,然后将它们逐一加入到这个集群当中,直到集群的节点数等于 size。

对于其他每一个节点,Operator 只需要执行如下两个操作即可

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_44

Etcd Operator 在业务逻辑的实现方式上,与常规的自定义控制器略有不同。我把在这一部分的工作原理,提炼成了一个详细的流程图

Kubernetes 学习02  容器编排与Kubernetes作业管理_进程组_45

Etcd Operator 的特殊之处在于,它为每一个 EtcdCluster 对象,都启动了一个控制循环,“并发”地响应这些对象的变化。显然,这种做法不仅可以简化 Etcd Operator 的代码实现,还有助于提高它的响应速度

而一个 Cluster 对象需要具体负责的,其实有两个工作。

其中,第一个工作只在该 Cluster 对象第一次被创建的时候才会执行。这个工作,就是我们前面提到过的 Bootstrap,即:创建一个单节点的种子集群。

Cluster 对象的第二个工作,则是启动该集群所对应的控制循环

这样,当这个容器启动之后,一个新的 Etcd 成员节点就被加入到了集群当中。控制循环会重复这个过程,直到正在运行的 Pod 数量与 EtcdCluster 指定的 size 一致。

在有了这样一个与 EtcdCluster 对象一一对应的控制循环之后,你后续对这个 EtcdCluster 的任何修改,比如:修改 size 或者 Etcd 的 version,它们对应的更新事件都会由这个 Cluster 对象的控制循环进行处理。