一、Kubernetes的扩展机制
1.1 扩展Kubernetes
即调整Kubernetes的工作方式,以让其能够支持新的资源类型、硬件类型等特性。使得Kubernetes具备高度可配置、可扩展的能力。扩展点包括:
- kubectl插件
- API Server扩展身份认证、鉴权和动态准入控制相关的插件
- API扩展,以支持更多的资源类型
- 调度器扩展以支持更多调度算法
- 控制器扩展以支持更多的Controller或Operator
- 网络插件,扩展Kubelet以配置Pod网络
- 设备插件,扩展Kubelet以支持更多的硬件,例如GPU
- 存储插件,扩展Kubelet以支持更多的存储卷类型
1.2 kubectl插件
kubectl会自动加载环境变量$PATH所指定的路径下以“kubectl-”为前缀的可执行程序,并以之作为插件。安装插件时,将此类的程序文件放置于环境变量$PATH所指定的任意一个路径下即可。常用插件列表:https://github.com/ishantanu/awesome-kubectl-plugins。“kubectl plugin list”命令可列出所有已经安装的插件,但是插件程序文件名称不能与kubectl的现有命令相同,kubectl不会加载这类插件。
可使用Krew管理kubectl插件。Krew是由Kubernetes SIG CLI社区维护的插件管理器,常用于发现和安装kubectl插件。Krew自身也是kubectl的插件之一。下载地址:https://github.com/kubernetes-sigs/krew/releases
wget https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz
tar -xf krew-linux_amd64.tar.gz
mv krew-linux_amd64 /usr/local/bin/kubectl-krew
之后可以用kubectl krew命令去接后面的选项。Krew的常用命令(选项):
- update:更新索引
- search:搜索插件
- install:安装插件
- info:打印插件相关的信息
- upgrade:升级安装的插件
- uninstall:卸载插件
kubectl krew search tree
kubectl krew install tree
kubectl tree deploy demoapp
1.3 API扩展
也称为资源客制化。资源是Kubernetes API中的端点,自定义资源便是对这类端点的扩展。自定义资源以动态注册的方式添加至API Server。
资源本身仅能用来存取结构化数据,其声明式API的特性,依赖于专用的自定义控制器。比如Calico部署后会新增新的名称空间、自定义的资源对象,而卸载了Calico,这些新的名称空间和资源对象又没了。添加自定义资源的途径:
- API Server源代码二次开发。但版本更新较难,同时受编程语言限制(golang)。
- CRD(CustomResourceDefinitions)
它是APIServer内置的资源类型,以创建资源对象的方式进行定义CRD对象。比如,创建了一个名为abc的CRD类型的对象,此时abc它是个新的资源类型(但相对于CRD它是对象,这个比较诡异),根据abc这个资源类型的定义去创建abc-1这个具体的资源对象(叫CR)。abc-1就好比一个Pod实例,abc就类似于Pod这个资源对象。但是,创建完CRD得有Operator或者Controller去支持自定义管理。多说一下,Operator 可以通过扩展 Kubernetes API,为应用程序添加更多自定义的管理能力。这样,用户可以通过 K8S API 进行更多高级功能的调用。
- API Aggregation
需要开发独立的API Server,在其中添加自定义资源,注册到API Server中的Aggerator程序,由Aggerator代理和转发请求。即当访问自定义资源类型时,Aggerator将请求转发给用户额外部署的API Server。
API Server是Kubernetes的唯一访问入口,默认客户端的所有操作都是发送给API Server进行响应,我们自定义的API Server要想能够被客户端访问,就必须通过内建API Server中的Aggregator组件中的路由信息,把对应路径的访问路由至对应API Server进行访问。而对应Aggregator中的路由信息,由Kubernetes内建API Service资源进行定义。API Service是在主API Server和聚合层上注册外部API Server的配置接口。
总结一下就是API Service资源就是用来定义原生API Server中Aggregator组件上的路由信息,该路由就是将某某端点的访问路由至对应API Server。所以,部署了外置的API Server后还要在内置的API Server内创建API Service对象用于添加路由规则,告诉内置API Server的聚合器把哪个资源群组下的自定义资源类型都转给哪个自定义API Server。
较之CRD,在处理性能、策略、认证、鉴权等方面提供了更多的可能性。
1.4 创建和使用CRD
用户通过创建CRD资源对象,来定义新的资源类型。再通过定义新的资源类型下的对象,来创建CR。这些通过声明式API定义的CR,其实际意义依赖于自定义的Controller或Operator。Kubernetes API Server负责处理自定义资源对象的存储。
列出现有的CRD:kubectl get crds
定义CRD的关键字段如下:
- group <string>:资源所属的组。
- versions <[]CustomResourceDefinitionVersion>:版本定义。其中name为版本号,schema为各字段的定义。
- scope <string>:作用域,集群级别或名称空间级别。
- names <CustomResourceDefinitionNames>:资源名称定义。
二、Kubernetes调度器
2.1 Kubernetes调度相关的概念
在Kubernetes中,调度是指为新创建的Pod挑选一个最佳运行节点。控制平面组件kube-scheduler是Kubernetes集群的默认调度器。
2.2 调度逻辑
正常情况下,kube-schuduler有个调度队列,所有等待调度的Pod(包括未被调度成功的Pod)都会被放置在此队列中。此队列通常是一个先进先出的队列。此时,Kubernetes从队列中挑个Pod出来,在集群内的所有或部分节点上针对这个Pod占用的资源(运行需求)进行评估,选取更合适的节点并把Pod调度给该节点。之后从调度队列取出Pod2、Pod3...Podn做类似操作。如果没有任何节点满足Pod的调度需求,则该Pod处于Pending状态,直到出现合适的节点为止。
2.3 调度器的调度流程
首先进行过滤(也叫预选),选出可满足Pod运行条件需求的可调度节点。这些需求包括:满足Pod的资源需求、满足与其它Pod间的特殊关系限制、满足同节点间的限制条件、确保整个集群资源得到合理利用。
其次为选出的各个可调度节点进行打分(也叫优选),根据分值高低进行排序,分值高的被选为合适节点,Pod调度到此节点上。
最后,调度器将调度决定告知kube-apiserver,此过程也叫绑定。调度器将Pod的spec.nodeName填上调度结果的Node名字。
2.4 预选
预选的功用,大体相当于节点过滤器(Filter)。它基于调度策略,从集群的所有节点中,过滤出符合条件的节点。执行具体过滤操作的是一组预选插件。
常用调度算法说明:
- PodFitsHost:检查的是宿主机的名字是否跟Pod的spec.nodeName一致。
- PodFitsHostPorts:检查Pod的各Containers中声明的Ports是否已经被节点上现有的Pod所占用。
- MatchNodeSelector:检查Pod的spec.affinity.nodeAffinity和spec.nodeSelector的定义是否同节点的标签相匹配。
- PodFitsResources:检查Pod的资源需求是否能被节点上的可用资源量所满足。
- PodToleratesNodeTaints:检查Pod是否能够容忍节点上的污点。
- MaxCSIVolumeCount:检查Pod依赖的由某CSI插件提供的PVC,是否超出了节点的单机上限。
- VolumeZonePredicate:检查持久化Volume的Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配。
- MatchInterPodAffinity:检查Pod间的亲和和反亲和定义是否得到满足。
- EvenPodsSpread:为一组Pod设定在指定TopologyKey上的散置要求,即打散一组Pod至不同的拓扑位置。
2.5 资源需求和资源限制
资源需求(requests),定义需要系统预留给该容器使用的资源最小可用值。容器运行时可能用不到这些额度的资源,但用到时必须确保有相应数量的资源可用。资源需求的定义会影响调度器的决策。
资源限制(limits)定义该容器可以申请使用的资源最大可用值,超出该额度的资源使用请求将被拒绝。该限制需要大于等于requests的值,但系统在其某项资源紧张时,会从容器那里回收其使用的超出其requests值的那部分。
requests和limits定义在容器级别,主要围绕cpu、memory、hugepages和ephemeral-storage四种资源。字段分别为spec.containers[].resources.limits和spec.containers[].resources.requests.。
所有那些不属于kubernetes.io域的资源,即为扩展资源(Extended resources),例如“nvidia.com/gpu ”。
Pod QOS的类别:
- Guaranteed。QOS等级最高,完全可靠。CPU和Memory都要满足条件:requests ==limits,其它资源类型不作限制。
- Burstable。QOS等级次之,比较可靠。CPU和Memory其一满足条件:requests != limits,该类别的适用条件最为宽泛。
- BestEffort。QOS等级最低,不太可靠。所有资源上都没有设定requests和limits。
通常对于核心业务,是需要设置资源需求及限制的,而且二者相同。当系统资源紧缺时,如果需要杀死Pod(比如OOM Kill),先杀死BestEffort类型的Pod,再杀死Burstable类型的Pod,最后处理Guaranteed类型的Pod。对于同一级别的Pod,看看哪个Pod实际占用资源占requests比例高,requests和limits的差距大,就优先干掉哪个Pod。
三、亲和调度
3.1 Node亲和调度
基于Pod和Node关系的调度策略,称为Node亲和调度。一个Pod更亲近某种特性的Node,则认为Pod与这样的Node具有亲和性,而不具有某种特征的Node就跟这个Pod不具有亲和性。换句话说,是否具有亲和性由节点自身的特性决定。何时可用到亲和调度?
- Pod的运行依赖于特殊硬件,例如SSD或GPU,但这些设备仅部分节点具备。
- 具有高计算量要求的Pod,也可能需要限制运行在特定的节点。设定Pod满足节点亲和的方式:
- pod.spec.nodeSelector,它是定向调度机制的实现之一。
- pod.spec.affinity.nodeAffinity,支持基于软亲和和硬亲和两种逻辑实现更精确的控制。
3.2 硬亲和和软亲和
硬亲和:requiredDuringSchedulingIgnoredDuringExecution,必须满足的亲和约束,必须调度至满足条件的节点。约束的影响仅作用于调度发生期间。
软亲和,preferredDuringSchedulingIgnoredDuringExecution。有倾向性的亲和约束,不同的约束条件存在不同的权重,约束评估同样仅发生在调度期间。优先将Pod调度至更为满足条件(权重更高)的节点。
节点亲和调度的语法规范如下:
spec:
...
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
preference <NodeSelectorTerm> # 节点选择器
matchExpressions <[]NodeSelectorRequirement> # 节点标签选择器表达式
key <string> # 键
operator <string> # 操作符
values <[]string> # 值列表
matchFields <[]NodeSelectorRequirement> # 节点字段选择器表达式,格式同标签选择器
weight <integer> # 权重
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms <[]NodeSelectorTerm> # 节点选择器列表
matchExpressions <[]NodeSelectorRequirement>
matchFields <[]NodeSelectorRequirement>
3.3 Pod的亲和和反亲和
基于Pod和Pod间关系的调度策略,称为Pod亲和调度(Affinity),以及Pod反亲和调度(AntiAffinity)。这里的亲和性主要说明待调度Pod是否要运行在某个节点上取决于该节点上是否运行有该Pod期望与其运行在一起的Pod或期望不要同其运行在一起的Pod。
Pod的亲和调度也具有硬亲和和软亲和。
- 硬亲和:requiredDuringSchedulingIgnoredDuringExecution:必须同某些Pod调度至同一位置(拓扑域)。
- 软亲和:preferredDuringSchedulingIgnoredDuringExecution:优先同某些Pod调度至同一位置(一个服务器或同一个机柜内的服务器)。
Pod反亲和调度的作用:避免单点故障、防止资源竞争。同样也具有硬亲和和软亲和。参数介绍同上。语法规范如下:
spec:
...
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution <[]WeightedPodAffinityTerm>
podAffinityTerm <PodAffinityTerm> # Pod亲和约束条件
labelSelector <LabelSelector> # 标签选择器,挑选评估同其亲和性的目标Pod
namespaceSelector <LabelSelector> # 名称空间选择器,用于限制目标Pod
namespaces <[]string> # 名称空间列表,用于限制目标Pod
topologyKey <string> # 拓扑键,用于分组节点,同组即为同一位置
weight <integer> # 权重
requiredDuringSchedulingIgnoredDuringExecution <[]PodAffinityTerm>
3.4 Pod拓扑分散约束
它是定义跨拓扑域分配工作负载的策略,常用于在不显著增加成本的情况下增强抵御系统故障的能力。即使某一可用区(在公有云上)出现故障,其它可用区仍然存在可用Pod。
拓扑域是指按节点标签定义和分组的节点集合。例如,区域、可用区、机架、主机名都可以作为将节点划分成不同拓扑域的依据。Kubernetes使用Topology Key(拓扑键)来定义将节点划分至不同拓扑域的标准。
有了拓扑域就有散置偏差(skew)。散置偏差指的是Pod打散调度至多个拓扑域后,拥有Pod数量最多的拓扑域,与拥有Pod数量最少的拓扑域之间的差值。
Pod分散约束策略基于允许的最大偏差(maxSkew)来定义Pod在拓扑域中的分散逻辑。无法满足最大偏差约束时,whenUnsatisfiable则用于定义妥协策略。
- DoNotSchedule (默认) :不予调度,Pod将处于Pending状态,实现的是硬限制。
- ScheduleAnyway:根据每个节点的skew值打分排序后进行调度,因而实现的是软限制。
四、Node Taints和Pod Tolerations调度
Node Affinity(节点亲和性),用于定义Pod对Node的倾向性,即基于Node的特定属性(标签或字段)来吸引特定的Pod。
而Node Taints(污点)则产生的是反向作用力,它用于让Node来排斥特定的Pod,仅那些能够容忍Node Taints的Pod才能运行于该节点上。Pod上定义的用于容忍Node Taints的属性,称为Pod容忍度(Toleration)。Taint和Toleration相互配合,可以用来阻止调度器将Pod分配到不适用的节点上。
节点亲和性是仅作用于调度决策当中,一旦调度决策完成、绑定完成,影响就不再产生(哪怕节点标签或Pod之上的约束定义发生变动都不再产生影响)。而基于污点驱逐的逻辑不仅作用于调度决策过程(即Pod能否允许节点污点),即使调度决策完成也仍然有可能产生影响(调度完成后节点污点发生变动或Pod容忍度发生变动导致二者不匹配,此时即使Pod绑定上来也照样有可能被驱逐出节点)。
4.1 Node Taints
节点属性,定义在spec.taints字段上。也可由“kubectl taint”命令进行添加或移除等管理操作。
kubectl taint nodes NAME KEY_1=VAL_1:TAINT_EFFECT_1 ... KEY_N=VAL_N:TAINT_EFFECT_N [options]
污点效用:指示该污点不能得到容忍时,将如何影响调度。即一个Pod能否运行于当前节点上或调度给当前节点以及调度后又是否产生影响。以下三个选项严重等级按顺序由低到高。
- PreferNoSchedule:不能容忍该污点的Pod,要尽量避免调度至该节点。
- NoSchedule:不能容忍则不许调度至该节点,但仅在调度决策执行期间产生影响,不影响已经运行于该节点的Pod。
- NoExecute:不能容忍则不许调度至该节点,且将会对已经运行于该节点的Pod产生影响。
- 不能容忍该污点的Pod将被立即驱逐。
- 若Pod能够容忍该污点,且Pod的容忍度未定义tolerationSeconds,则Pod可以一直运行于该节点。
- 若Pod能够容忍该污点,且Pod的容忍度定义了tolerationSeconds,则Pod运行指定的时长后会被驱逐。
4.2 Pod Tolerations
用于管理Pod的容忍度。它是Pod属性之一,定义在“spec.tolerations”字段上,数据类型为列表值。每个列表项由key、operator、value、toleratinSeconds和effect几个字段组成。
- key:容忍度键,即容忍的污点键。
- operator:操作符,仅支持“Equal”和“Exists”两个。
- value:键的值。
- effect:容忍的污点效用,可用值与Node Taint相同。
- tolerationSeconds:NoExecute效用下,当前Pod可容忍某污点时,在驱逐前容许运行的时长。
注意:容忍度的污点效用要包括(>=)污点的效用才表示它能容忍该污点。比如污点的效用为NoExecute,而容忍度的污点效用为PreferNoSchedule,则它是不能容忍的。
如何评估容忍度是否匹配污点?一个容忍度“匹配”到一个污点,是指二者之间具有同样的key和effect,并且满足如下两个条件之一:operator是Exists;operator是Equal,且二者的value相同。
注意以下特殊情形:
- 若某个容忍度的key为空,且operator为Exists,表示其可以容忍任意污点。
- 若effect为空,则可以匹配所有键名相同的污点。
4.3 容忍度与污点的匹配机制
节点上可定义多个Taints,Pod上也可定义多个Tolerations,针对一个节点,调度器调度决策过程如下:
首先,遍历该节点的所有污点,并过滤掉容忍度可匹配到的污点;其次,余下未被过滤掉的污点,则由污点的effect来判定其满足状态:
- 若存在至少一个效用为NoSchedule的污点,则Pod不会被调度至该节点。
- 若不存在效用为NoSchedule的污点,但至少存在一个效用为PreferNoSchedule的污点,则调度器会尝试避免将Pod调度至该节点。
- 若存在至少一个效用为NoExecute的污点,则Pod不会被调度至该节点;若Pod已经运行于该节点,则Pod还将被驱逐。
4.4 基于污点的驱逐
节点状态相关的某种条件(Node Conditions)为真时,节点控制器会自动给节点添加一个污点,当节点负载过高时就不要再让调度器调度其它资源对象到节点来。
- node.kubernetes.io/not-ready:节点未就绪,即节点状况Ready的值为 "False"。
- node.kubernetes.io/unreachable:节点控制器访问不到节点,即节点状况Ready的值为 "Unknown"。
- node.kubernetes.io/memory-pressure:节点内存资源紧张。
- node.kubernetes.io/disk-pressure:节点磁盘空间资源紧张。
- node.kubernetes.io/pid-pressure:节点PID资源紧张。
- node.kubernetes.io/network-unavailable:节点网络不可用。
- node.kubernetes.io/unschedulable:节点不可用于调度。
- node.cloudprovider.kubernetes.io/uninitialized:cloud provider尚未进行初始化。
控制平面基于Node Controller自动创建与节点状况对应的、效果为NoSchedule的污点。调度器在进行调度时会检查节点上的污点,而非检查节点状况。新建Pod时,可以通过添加相应的容忍度来忽略节点状况。
Master节点通常会被添加污点,以避免非控制平面应用运行到这类节点上。kubeadm部署的集群会自动为Master添加如下污点:
key: node-role.kubernetes.io/control-plane
effect: NoSchedule