APIServer职责主要是认证,鉴权,准入,它判断一个请求是谁发起的,发起人有没有相应的权限,这个请求是不是合法的,以及从apiserver这端觉得要对原始请求变更一些属性,那么它就可以在这里面去做。
apiserver经过这些环节之后,这些检查都通过了,请求也合法,那么它会将请求存在etcd里面。
apiserver本身是k8s集群当中唯一和etcd数据库通信的这样一个组件,其他所有组件都需要和apiserver去通信,去获取数据的变更信息。
kubelet分为自己的框架代码,以及包含下面的接口抽象,它将运行时抽象为cri,将网络抽象为cni,将存储抽象为csi。
kube-scheduler
kube-scheduler负责分配调度Pod到集群内的节点上,它监听kube-apiserver,查询还未分配Node 的 Pod,然后根据调度策略为这些Pod分配节点(更新 Pod 的 NodeName 字段)。调度器需要充分考虑诸多的因素∶
- 公平调度(当接受到很多请求的时候,确保我们能够公平的去处理请求,大家都是平等的,本着先到先服务的原则去做调度,这是公平性。还有一些不公平的因素,比如调度的时候有调度优先级,我的一个应用特别重要,希望插队放到前面去,所以在这k8s提供了完备的支持,对于同一个调度优先级,我是公平的原则,不同的优先级会插队放在前面,优先级越高的越优先调度)
- 资源高效利用(找到最合适的节点调度过去)
- QoS
- Affinity和Anti-Affinity
- 数据本地化(data locality)
- 内部负载干扰(inter-workload interference)
- deadlines。
调度器会去监听集群里面所有计算节点的信息,它要知道当前集群里面有多少个计算节点,这些节点的健康状态如何,它们的资源使用情况如何,有多少资源使用了,有多少可分配。
每个计算节点都会将自己的信息上报给apiserver,我们的调度器就会去watch apiserver,获取这些节点的信息,那么调度器就有一个集群的全局视图。
一方面它有集群所有节点的计算资源的全局视图,另外一方面它能够接受到用户的pod,对它来说就是调度请求,来寻找一个最佳节点,找到最佳节点就完成了pod和节点的绑定关系。本质上就是将pod的nodename字段填充了。
用户建立pod的时候是不去填nodename的,因为不知道pod会被调度到哪,调度器会去看一个pod的nodename为空,那就说明你要去调度的,所以它就会去做调度,找到合适的节点,将nodename填上去。
调度器
kube-scheduler调度分为两个阶段,predicate和priority∶
- predicate∶过滤不符合条件的节点,filter
- priority∶优先级排序,选择优先级最高的节点,score
filter:有100台机器,你有一个pod请求,我得先去看看有哪些节点不满足你的需求,先将不满足需求的节点过滤掉。
过滤之后可能还剩下10台能够满足你的需求,那么就需要排序了,排序就是按照各种因素去打分了。
Predicates策略
因为调度的时候,需要考虑诸多因素,每个因素对于调度器来说都是一个插件。做predicate的过程相对于遍历这些predict插件,然后一个个去执行。
PodFitsResources:检查Node的资源是否充足,包括允许的Pod数量、CPU、内存、GPU个数以及其他的OpaquelntResources。(先看看哪些节点是不满足pod资源的,没有合适资源机器全部刷掉)
PodFitsHostPorts:检查是否有Host Ports冲突。
PodFitsPorts: 同PodFitsHostPorts。(有些pod希望占用主机端口,那我去调度的时候要去看这个端口还是不是空余的,如果这个端口被占用了,说明这个节点就不能安置这个pod了)
HostName∶ 检查pod.Spec.NodeName是否与候选节点一致。
MatchNodeSelector∶ 检查候选节点的pod.Spec.NodeSelector是否匹配。(只会调度到这些节点)
NoVolumeZoneConflict∶ 检查volume zone 是否冲突。
MatchlnterPodAffinity∶检查是否匹配Pod的亲和性要求。
NoDiskConflict∶ 检查是否存在Volume冲突,仅限于GCEPD、AWS EBS、Ceph RBD 以及 iSCSI。
PodToleratesNodeTaints∶ 检查Pod是否容忍Node Taints。
CheckNodeMemoryPressure∶ 检查Pod是否可以调度到MemoryPressure的节点上。
CheckNodeDiskPressure∶ 检查Pod是否可以调度到DiskPressure的节点上。
NoVolumeNodeConflict∶ 检查节点是否满足Pod所引用的Volume的条件。
还有很多其他策略,你也可以编写自己的策略。
Predicates plugin 工作原理
当去做pod调度的时候,就会一个个去遍历predicate的plugin,我就一个一个plugin去跑,经过每一个plugin,我都会过滤一批机器,经过每一个plugin都会过滤掉一批机器,最后就剩下符合调度需求的机器。
Priorities策略
对于priority来说也有很多的插件,针对每个插件,他也是去遍历每个插件去算分,最后会给每个节点打分汇总,最终将得分最高的节点排在前面。
SelectorSpreadPriority∶优先减少节点上属于同一个Service或Replication Controller的Pod数量。
InterPodAffinityPriority∶优先将Pod调度到相同的拓扑上(如同一个节点、Rack、Zone等)。
LeastRequestedPriority∶优先调度到请求资源少的节点上。
BalancedResourceAllocation∶优先平衡各节点的资源使用。
NodePreferAvoidPodsPriority∶ alpha.kubernetes.io/preferAvoidPods字段判断,权重为10000,避免其他优先级策略的影响。
资源需求
CPU
- requests:Kubernetes 调度 Pod 时,会判断当前节点正在运行的 Pod 的 CPU Request 的总和,再加上当前调度Pod的CPU request,计算其是否超过节点的CPU的可分配资源
- limits:配置cgroup以限制资源上限。
内存
- requests:判断节点的剩余内存是否满足Pod的内存请求量,以确定是否可以将Pod调度到该节点。
- limits:配置cgroup以限制资源上限。
磁盘资源需求
容器临时存储(ephemeral storage)包含日志和可写层数据,可以通过定义 Pod Spec 中的limits.ephemeral-storage和requests.ephemeral-storage来申请。
Pod 调度完成后,计算节点对临时存储的限制不是基于 cgroup 的,而是由 kubelet 定时获取容器的日志和容器可写层的磁盘使用情况,如果超过限制,则会对Pod进行驱逐。
Init Container的资源需求
在一个pod里面除了主容器,还有init container,做些初始化的工作,istio就有initcontainer,它起来之后会去配置本地的iptables规则,配置完之后就退出了。
比如说应用要通过jwt token去访问其他的应用,应用和应用之间需要做认证鉴权的,我们就会使用初始化容器,因为这个token是一次性获取的,我们就会使用初始化的容器去获取这个token,这个token获取完毕就存在本地硬盘,然后硬盘通过volume mount到一个主容器,和主容器mount到同一个路径,那么就可以被读取了。
initcontainer很多时候是主容器预先加载资源的时候,加载配置的时候就可以让其去做。
- 当 kube-scheduler调度带有多个init 容器的 Pod 时,只计算 cpu.request 最多的 init 容器,而不是计算所有的init容器总和。(也可以设置request limit)
- 由于多个init 容器按顺序执行,并且执行完成立即退出,所以申请最多的资源init 容器中的所需资源,即可满足所有init容器需求。
- kube-scheduler在计算该节点被占用的资源时,init 容器的资源依然会被纳入计算。因为init容器在特定情况下可能会被再次执行,比如由于更换镜像而引起Sandbox重建时。
把Pod调度到指定Node上
可以通过 nodeSelector、nodeAffinity、podAffinity以及Taints和tolerations等来将Pod调度到需要的Node上。
也可以通过设置 nodeName 参数,将Pod 调度到指定Node节点上。
比如,使用nodeSelector,首先给Node加上标签∶kubectl label nodes <your-node-name>disktype=ssd
接着,指定该Pod只想运行在带有disktype=ssd 标签的Node上。
nodeAffinity
nodeAffinity其实是上面nodeselector的扩展,硬亲和其实在predict阶段去做,在predicate阶段去看看节点满不满足我亲和性需求,如果满足才能作为备选。
软亲和是在priorities阶段去做,满足需求的排在前面,不满足需求的排在后面,就是参与打分。
nodeselector太单一了,后面就被亲和性和反亲和性替换掉了,可以认为节点的亲和性是nodeselector的一个演进。
nodeAffinity 目前支持两种∶ requiredDuringSchedulinglgnoredDuringExecution 和preferredDuringSchedulinglgnoredDuringExecution,分别代表必须满足条件和优选条件。
比如下面的例子代表调度到包含标签Kubernetes.io/e2e-az-name并且值为e2e-az1或e2e-az2的Node上,并且优选还带有标签 another-node-label-key=another-node-label-value 的 Node。
prefer其实是用来打分的,它有个属性叫weight,就是它打分的一个权重,你可以有很多prefer的规则,每个规则具体多少占比就是通过这个weight。
podAffinity
podAffinity基于Pod的标签来选择Node,仅调度到满足条件Pod所在的Node上,支持podAffinity和podAntiAffinity。这个功能比较绕,以下面的例子为例∶
如果一个"Node所在Zone中包含至少一个带有security=S1标签且运行中的Pod",那么可以调度到该Node,不调度到"包含至少一个带有security=S2标签且运行中Pod"的Node上。
Taints和Tolerations
Taints和Tolerations用于保证Pod不被调度到不合适的Node上,其中Taint应用于Node上,而
Toleration则应用于Pod上。
目前支持的Taint类型∶
- NoSchedule∶新的Pod不调度到该Node上,不影响正在运行的Pod
- PreferNoSchedule∶soft版的NoSchedule,尽量不调度到该Node上
- NoExecute∶新的Pod不调度到该Node上,并且删除(evict)已在运行的Pod。Pod可以增加一个时间(tolerationSeconds)。
k8s也在用taint,当一个节点不响应或者出现问题的时候,k8s就会将这批节点打上unhealthy这样的taint。这个taint的effect是NoExecute,然后tolerate second是600s,也就是在600s之后这个节点上面所有的pod都会被驱逐掉。
然而,当Pod的Tolerations匹配Node的所有Taints的时候可以调度到该Node上,当Pod是已经运行的时候,也不会被删除(evicted)。
另外对于NoExecute,如果Pod增加了一个tolerationSeconds,则会在该时间之后才删除Pod。
多租户Kubernetes 集群-计算资源隔离
特别是在公有云用户,有些场景是同一个控制平面,管理多个节点,这些不同的节点为不同的公有云租户服务的。那这些用户有些诉求,比如应用不能和其他人部署在一起,我希望完全隔离,那么就可以将不同的计算节点打上不同的taint。
k8s也在用taint,当一个节点不响应或者出现问题的时候,k8s就会将这批节点打上unhealthy这样的taint。这个taint的effect是NoExecute,然后tolerate second是600s,也就是在600s之后这个节点上面所有的pod都会被驱逐掉。这样其实保证了节点出现故障,那么这上面的pod是需要被排空的,这是k8s自身使用taint的一个场景。
优先级调度
总有些业务,比如关键业务会比其他业务优先级更加高,当我集群资源紧张的时候就有可能产生资源竞争,那我希望给业务定级,比如1 2 3 4 5,很多客户都有在线业务定级的需求,比如和支付相关的,这些都是第一级的,你后面支撑运维管理的平台优先级是低的,如果部署在同一个集群发生了资源竞争,那么我希望高优先的作业优先保证,怎么通过保证,那么就是通过调度优先级。
从v1.8开始,kube-scheduler支持定义Pod的优先级,从而保证高优先级的Pod优先调度。开启方法为∶
apiserver 配置--feature-gates=PodPriority=true 和--runtime-
config=scheduling.k8s.io/v1alpha1=true
kube-scheduler 配置--feature-gates=PodPriority=true
PriorityClass
调度优先级定义是调度器group里面提供了一个priorityclass对象,这个对象很简单。
第一你可以去定义一个value,这个value越大,优先级越高,是不是globaldefault,如果定义为globaldefault,那么集群里面没有打globaldefault的pod就全部是那个级别的。
还有一个属性是可不可以抢占,抢占的意思是高优先级的pod没有资源的时候在调度的时候发生了pending,那么kubernetes会去看当前集群里面所有的pod有没有比这个优先级更加低,如果有比我级别低的那么就将这个资源抢过来,直接将这些pod驱逐,然后让这些高优先级pod调度出去。
其实这就不公平了,因为有优先级,有高低贵贱了,高级的永远会去抢低级的资源。
为pod设置PriorityClass
怎么为pod设置priorityclass?第一你要为集群设置好priorityclass,其次你要为业务定级,指定优先级。
多调度器
如果默认的调度器不满足要求,还可以部署自定义的调度器。并且,在整个集群中还可以同时运行多个调度器实例,通过pod.Spec.SchedulerName来选择使用哪一个调度器(默认使用内置的调度器)。
一个集群可能不同场景下面调度需求是不一样的,在线业务使用kubernetes的默认调度器就行了,很多离线的业务有更加多的需求,第一它的调度频次非常的大,基于event的调度已经不适合了,可能一秒钟就来几百个调度,原生的调度器没有办法满足这么大的量了,有没有批次的调度器呢?
比如腾讯有TKE的调度器,这是面向批处理的一个调度器,比如批处理还有很多其他的调度需求,我一起要5个pod,要去帮我试一下这5个要一起全部满足需求再去调度,如果不满足就不调度,因为只启动一部分pod是没有意义的。
所以很多的需求是需要通过其他的调度器来实现的。一个集群里面可以运行多个调度器,你要去启动pod的时候可以通过SchedulerName来定义使用哪个调度器。如果指定就是默认的调度器。
来自生产的一些经验
集群里面调度器是没有熔断的,如果一个节点有问题,就会发生一种现象,就会将这个问题放大,比如集群里面一个节点出现问题了,这个节点上面应用跑不起来,可能runtime出现问题的,但是没有影响节点的状态,节点的状态还是正常的,这就会出现问题,用户去建pod的时候,这个节点肯定是资源最多的,这个时候应用调度过来然后发现跑不起来,用户就去删除,删除以后再经过重新调度,那么又调度到这个节点上面了,可能一个集群5000个节点,那么1-2个节点有这样一个问题,那么可能对于用户来说整个集群就不可用了。
因为没有熔断,这个问题没有被捕捉到,可能一些底层问题导致的,1-2个节点出现了问题,对于用户来说它是整个集群出现了问题。
应用炸弹:创建了一个pod,不停的fork子进程,或者有pid的leak,或者有connection leak,如果不限制会将整个节点资源全部消耗。无论是pid还是文件句柄还是connection。
消耗之后这个节点就会变为故障,not ready,那么not ready之后这个节点的pod就会被排空驱逐,然后这个pod又会被调度到另外一个节点,然后又会把第二个节点破坏掉,如此往复循环,这种破坏性就很大,那么新的版本里面就添加了一个pod可以使用多少pid,这些都是可以通过参数去限制。