本文由 CNCF + Alibaba 云原生技术公开课 整理而来
资源元信息
- Kubernetes 资源对象:
Kubernetes 的资源对象组成:主要包括了 Spec
和 Status
两部分。其中 Spec
部分用来描述期望的状态,Status
部分用来描述观测到的状态。
Labels
:
最重要的一个元数据:资源标签 Labels
,Labels
是一种具有标识型的 key: value
元数据。
示例:
environment: production
release: stable
app.kubernetes.io/version: 5.7.21
failure-domain.beta.kubernetes.io/region: cn-hangzhou
可以看到,前三个标签都打在了 Pod
对象上,分别标识了对应的应用环境、发布的成熟度和应用的版本。从应用标签的例子可以看到,标签的名字包括了一个域名的前缀,用来描述打标签的系统和工具。
Labels
主要用来筛选资源和组合资源,Kubernetes 可以使用类似于 SQL 的 select 查询,来根据 Labels
查询相关的资源。
Selector
:
Selector
就是标签选择器,Labels
不提供唯一性,通过 Selector
才能方便地辨识出一组对象。
API 目前支持两种 Selector
:基于相等条件的 和 基于集合条件的。一个 Selector
可以由多个必须条件组成,以 ,
分隔。在多个必须条件指定的情况下,所有的条件都必须满足,多个条件之间是逻辑“与”的关系。
基于相等条件的 Selector
允许用 Label
的键或者值进行过滤。匹配的对象必须满足所有指定的 Label
约束,尽管可能也有另外的 Label
。
有三种运算符是允许的:=
、==
和 !=
。前两种代表 相等,后一种代表 不相等。例如:
environment = production 选择所有键为 environment 且值等于 production 的资源
tier != frontend 选择所有键为 tier 且值不等于 frontend 的资源,和那些没有键为 tier 的label的资源
environment=production,tier!=frontend 选择所有键为 environment 且值等于 production,并且键为 tier 且值不等于 frontend 的资源
基于集合条件的 Selector
允许用一组值来过滤键。支持三种操作符:in
、notin
和 exists
(仅针对于key) 。例如:
environment in (production, qa) 选择所有键为 environment 且value等于 production 或者 qa 的资源
tier notin (frontend, backend) 选择所有键为 tier 且值是除了 frontend 和 backend 之外的资源,和那些没有label的键是 tier 的资源
partition 选择所有有一个 Label 的键为 partition 的资源,值是什么不会被检查
!partition 选择所有没有 Label 的键为 partition 的资源,值是什么不会被检查
,
相当于一个 AND 操作符。基于集合条件的 Selector
是一个相等性的宽泛的形式,因为 environment=production
相当于 environment in (production)
,!=
相当于 notin
。
基于集合的条件 可以与 基于相等的条件 结合起来使用,例如:partition in (customerA,customerB),environment!=qa
。
Annotations
:
Annotations
一般是系统或者工具用来存储资源的非标识性数据,用来增加资源的 Spec
、Status
的描述。
可以使用 Labels
或 Annotations
将元数据附加到资源对象。Labels
可用于选择资源对象并查找满足某些条件的资源对象集合。相比之下,Annotations
不用于标识和选择资源对象。Annotations
中的元数据可以包括 Labels
中不允许使用的字符。
Annotations
和 Labels
一样,也是由 key: value
组成:
service.beta.kubernetes.io/alicloud-loadbalancer-cert-id: you-cert-id
nginx.ingress.kubernetes.io/service-weight: "new-nginx: 20, old-nginx: 80"
注意:Annotations
不会被 Kubernetes 直接使用,其主要目的是方便用户阅读查找。
Ownereference
:
Ownereference
,即所有者,一般指集合类的资源。
集合类资源的控制器会创建对应的归属资源,比如:ReplicaSet
控制器在操作中会创建 Pod
,被创建 Pod
的 Ownereference
就指向创建 Pod
的 ReplicaSet
。
Ownereference
使得用户可以方便地查找一个创建资源的对象,另外还可以用来实现级联删除的效果。
控制器模式
- 控制循环:
控制型模型最核心的就在于控制循环,在控制循环中,包括三个逻辑组件:控制器、被控制系统、能够观测系统的传感器。
外界通过修改资源 Spec
来控制资源,控制器比较资源 Spec
和 Status
,从而计算一个 diff, diff 用来决定执行对系统进行怎样的控制操作,控制操作会使系统产生新的输出,并被传感器以资源 Status
形式上报,控制器的各个组件都是独立自主地运行,不断使系统向 Spec
终态趋近。
- 传感器:
控制循环中的传感器主要由 Reflecotor、Informer、Indexer 三个组件构成。
Reflecotor 通过 List
和 Watch
K8s Server 来获取资源的数据。List
用来在 Controller
重启 及 Watch
中断的情况下,进行系统资源的全量更新;而 Watch
则在多次 List
之间进行资源的增量更新;Reflecotor 在获取新的资源数据后,会在 Delta 队列中塞入一个包括资源对象信息本身以及资源对象事件类型的 Delta 记录, Delta 队列中保证同一个对象在队列中仅有一条记录,从而避免 Reflecotor 重新 List
和 Watch
的时候产生重复的记录。
Informer 组件不断地从 Delta 队列中弹出 Delta 记录,然后把资源对象交给 Indexer,让 Indexer 把资源记录在一个缓存中,缓存在默认配置下是用资源的命名空间来做索引的,并且可以被 Controller Manager
或多个 Controller
共享。之后,再把这个事件交给事件的回调函数。
控制循环中的控制器组件主要由事件处理函数以及 worker 组成,事件处理函数之间会相互关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理。对于需要处理的事件,会把事件关联资源的命名空间以及名字塞入一个工作队列中,并且由后续的 worker 池中的一个 worker 来处理,工作队列会对存储的对象进行去重,从而避免多个 worker 处理同一个资源的情况。
worker 在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或者更新系统对象,或者调用其他的外部服务。如果处理失败,一般会把资源的名字重新加入到工作队列中,从而方便之后进行重试。
- 控制循环示例:
ReplicaSet
是一个用来描述无状态应用的扩缩容行为的资源,ReplicaSet Controler
通过监听 ReplicaSet
资源来维持应用希望的状态数量,ReplicaSet
中通过 Selector
来匹配所关联的 Pod
。在这里考虑 ReplicaSet
扩容的场景。
首先,Reflector 会 Watch
到 ReplicaSet
和 Pod
两种资源的变化,发现 ReplicaSet
发生变化后,在 Delta 队列中塞入了对象,而且类型是更新的记录。
Informer 一方面把新的 ReplicaSet
更新到缓存中,并与 Namespace
作为索引;同时调用 Update
的回调函数,ReplicaSet Controler
发现 ReplicaSet
发生变化后会把字符串塞入到工作队列中,工作队列后的一个 worker 从工作队列中取到了这个字符串的 key,并且从缓存中取到了最新的 ReplicaSet
数据。
worker 通过比较 ReplicaSet
中 Spec
和 Status
里的数值,发现需要对这个 ReplicaSet
进行扩容,因此 ReplicaSet
的 worker 创建了一个 Pod
,这个 Pod
中的 Ownereference
指向了 ReplicaSet
。
然后 Reflector Watch
到 Pod
的新增事件,在 Delta 队列中额外加入了 Add 类型的 Delta 记录,一方面把新的 Pod
记录通过 Indexer 存储到缓存中,另一方面调用 ReplicaSet
控制器的 Add 回调函数,Add 回调函数通过检查 Pod OwnerReferences
找到对应的 ReplicaSet
,并把包括 ReplicaSet
命名空间和字符串塞入到工作队列中。
ReplicaSet
的 worker 在得到新的工作项之后,从缓存中取到了新的 ReplicaSet
记录,并得到了其所有创建的 Pod
。因为 ReplicaSet
的状态不是最新的,也就是所有创建 Pod
的数量不是最新的,所以在此时 ReplicaSet
更新 Status
使得 Spec
和 Status
达成一致。
API 设计
- API 设计方法:
Kubernetes 控制器模式依赖声明式 API。 另一种常见的 API 类型是命令式 API。
两种 API 在交互行为上存在差别。在生活中,常见的命令式的交互方式是家长和孩子的交流方式,因为孩子缺乏目标意识,无法理解家长期望,家长往往通过一些命令叫孩子做一些事情,比如吃饭、睡觉、刷牙类似的命令。在容器编排体系中,命令式 API 就是通过向系统发出明确的操作来执行的。
而生活中常见的声明式交互方式,就是老板和员工的交流方式。老板一般不会对员工下很明确的指令,而是通过设置可量化的业务目标,让员工自己完成,至于如何完成业务目标老板并不关心。类似的,在容器编排体系中,可以编辑 YAML 文件,指定 Pod
副本数保持几个,而不是明确的去增加 Pod
或 删除已有 Pod
来保持副本数。
- 命令式 API 的问题:
命令式 API 最大的一个问题在于错误处理。在大规模的分布式系统中,错误是无处不在的,一旦发出的命令没有响应,调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题。
实际上许多命令式的交互系统后台还会做一个巡检系统,用来修正命令处理超时、重试等场景造成的数据不一致的问题。由于巡检逻辑和日常操作逻辑是不一样的,往往在测试上覆盖不够,在错误处理上不够严谨,具有很大的操作风险,因此很多巡检系统往往都是人工来触发的。
最后,命令式 API 在处理多并发访问时,也很容易出现问题。
- 声明式 API 的优势:
相对的,声明式 API 系统里天然地记录了系统现在和最终的状态。不需要额外的操作数据,另外由于状态的幂等性,可以在任意时刻反复操作。
在声明式系统运行的方式里,正常的操作实际上就是对资源状态的巡检,不需要额外开发巡检系统,系统的运行逻辑也可以在日常运行中得到测试和锤炼,因此整个操作系统的稳定性能够得到保证。
最后,因为资源的最终状态的明确的,所以可以合并多次对状态的修改,可以不需要加锁就支持多并发访问。
- 控制器模式总结:
Kubernetes 所采用的控制器模式,是由声明式 API 驱动的,确切来说是基于对 Kubernetes 资源对象的修改来驱动的。
Kubernetes 资源背后,是关注该资源的控制器,这些控制器将异步的控制系统想设置的 Spec
期望状态趋近。这些控制器又是自主运行的,使得系统的自动化和无人值守称为可能。
Kubernetes 的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态的应用,往往可以通过自定义资源和控制器的方式,来自动化运维操作。这正是 Kubernetes Operator 的使用场景。