[云原生] Kubernetes 控制平面组件:调度器和控制器
调度
kube-scheduler负责分配调度Pod到集群内的节点上,它监听kube-apiserver,查询还未分配Node的Pod,然后根据调度策略为这些Pod分配节点(更新Pod的NodeName字段)。需要考虑的信息如下所示:
- 公平调度(顺序)
- 资源高效利用
- QoS
- affinity和anti-affinity
- 数据本地化(data locality)
- 内部负载干扰(inter-workload interface)
- deadlines
kube-scheduler调度分为两个阶段,predicate 和 priority,predicate过滤不符合条件的节点,priority选择高优先级的节点。Predicate策略有多个,内置的Predicate策略如下所示。Predicates plugin工作原理是一层层运行插件并过滤,最后剩可用的节点集合,然后就会往Priority策略。
- PodFitsHostPorts:检查是否有Host Ports冲突
- PodFitsPorts:同PodFitsHostPorts。
- PodFitsResrouces:检查Node的资源是否充足,包括允许的Pod数量、CPU、内存、GPU个数以及其他的OpaqueIntResources。
- HostName:检查pod.Spec.NodeName是否与候选节点一致。
- MatchNodeSelector:检查候选节点的pod.Spec.NodeSelector是否匹配。
- NoVolumeZoneConflict:检查volume zone是否冲突。
- MatchinterPodAffinity:检查是否匹配Pod的亲和性要求。
- NoDiskConflict:检查是否存在Volume冲突,仅限于GCEPD、AWSEBS、Ceph RBD以及i5CSl。
- PodToleratesNodeTaints:检查Pod是否容忍Node Taints。
- CheckNodeMemoryPressure:检查Pod 是否可以调度到MemoryPressure的节点上。
- CheckNodeDiskPressure:检查Pod是否可以调度到DiskPressure的节点上。
- NoVolumeNodeConflict:检查节点是否满足Pod所引用的Volume的条件。
Priorities策略的工作原理是根据每一个插件会有权重打分机制,然后最后权重分数高的择优。
- SelectorSpreadPriority:优先减少节点上属于同一个Service或Replication Controller的Pod数量(备份冗余)。
- InterPodAffinityPriority:优先将Pod调度到相同的拓扑上(如同一个节点、Rack、Zone等)。
- LeastRequestedPriority:优先调度到请求资源少的节点上。
- BalancedResourceAllocation:优先平衡各节点的资源使用。
- NodePreferAvoidPodsPriority:alpha.kubernetes.io/preferAvoidPods字段判断,权重为10000,避免其他优先级策略的影响。
- NodeAffinityPriority:优先调度到匹配NodeAffinity的节点上。
- TaintTolerationPriority:优先调度到匹配TaintToleration的节点上。
- ServiceSpreadingPriority:尽量将同一个service的Pod分布到不同节点上,已经被SelectorSpreadPriority替代(默认未使用)。
- EqualPriority:将所有节点的优先级设置为1(默认未使用)。
- ImageLocalityPriority:尽量将使用大镜像的容器调度到已经下拉了该镜像的节点上(默认未使用)。
- MostRequestedPriority:尽量调度到已经使用过的Node上,特别适用于cluster-autoscaler(默认未使用)。
资源需求
CPU
- requests
Kubernetes调度Pod时,会判断当前节点正在运行的Pod的CPU Request的总和,再加上当前调度Pod的CPU request,计算其是否超过节点的CPU的可分配资源。 - limits
配置cgroup以限制资源上限。
内存
- requests
判断节点的剩余内存是否满足Pod的内存请求量,以确定是否可以将Pod调度到该节点。 - limits
配置cgroup以限制资源上限。
cpu, request调整 cpu.shares; limit调整cpu.cfs_quota_us
Kubernetes 创建 Pod 时就给它指定了下列一种 QoS 类:Guaranteed,Burstable,BestEffort。
- Guaranteed:Pod 中的每个容器,包含初始化容器,必须指定内存和CPU的requests和limits,并且两者要相等。
- Burstable:Pod 不符合 Guaranteed QoS 类的标准;Pod 中至少一个容器具有内存或 CPU requests。
- BestEffort:Pod 中的容器必须没有设置内存和 CPU requests或limits。
结合节点上 Kubelet 的CPU管理策略,可以对指定 pod 进行绑核操作,例如,可以通过 static 策略,针对具有整数型 CPU requests 的 Guaranteed Pod ,允许该类 Pod 中的容器访问节点上的独占 CPU 资源。这种独占性是使用 cpuset cgroup 控制器 来实现的。诸如容器运行时和 kubelet 本身的系统服务可以继续在这些独占 CPU 上运行。独占性仅针对其他 Pod。这里cpuset基本功能是限制某一组进程只运行在某些cpu和内存节点。
不同 QoS 的 Pod 具有不同的 OOM 分数,当出现资源不足时,集群会优先 Kill 掉 Best-Effort 类型的 Pod ,其次是 Burstable 类型的 Pod ,最后是Guaranteed 类型的 Pod 。
对于Init Container的资源需求,由于多个init容器按顺序执行,并且执行完成立即退出,所以申请最多的资源init容器中的所需资源,即可满足所有init容器需求。
把Pod调度到指定Node上
可以通过nodeSelector、nodeAffinity、podAffinity以及Taints 和tolerations等来将Pod 调度到需要的Node上。
也可以通过设置nodeName参数,将Pod 调度到指定node节点上。
- nodeSelector
可以使用nodeSelector,首先给Node加上标签:
kubectl label nodesdisktype=ssd接着,指定该Pod 只想运行在带有 disktype=ssd标签的Node上。 - NodeAffinity
NodeAffinity目前支持两种: requiredDuringSchedulinglgnoredDuringExecution(必须满足条件); preferredDuringSchedulinglgnoredDuringExecution(优选条件) - 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类型:
a. NoSchedule:新的Pod不调度到该Node上,不影响正在运行的Pod;
b. PreferNoSchedule:soft版的NoSchedule,尽量不调度到该Node上;
c. NoExecute:新的Pod 不调度到该Node上,并且删除(evict)已在运行的Pod。Pod可以增加一个时间(tolerationSeconds)。
然而,当Pod的Tolerations 匹配Node的所有Taints的时候可以调度到该Node上;当Pod是已经运行的时候,也不会被删除(evicted)。另外对于NoExecute,如果Pod增加了一个tolerationSeconds,则会在该时间之后才删除 Pod。 - 优先级调度
从v1.8开始,kube-scheduler支持定义Pod的优先级,从而保证高优先级的Pod 优先调度。开启方法为:
apiserver配置-feature-gates=PodPriority=true和-runtime-
config=scheduling.k8s.io/vlalphal=true
kube-scheduler配置–feature-gates=PodPriority=true
Controller Manager
Controller Manager 由 kube-controller-manager 和 cloud-controller-manager 组成,是 Kubernetes 的大脑,它通过 apiserver 监控整个集群的状态,并确保集群处于预期的工作状态。
kube-controller-manager 由一系列的控制器组成:
- Certificate Controller
- ClusterRoleAggregation Controller
- CronJob Controller
- Node Controller
- Deployment Controller
- Daemon Controller
- StatefulSet Controller
- Endpoint Controller
- Endpointslice Controller
- Garbage Collector
- Namespace Controller
- Job Controller
- Pod AutoScaler
- PodGC Controller
- ReplicaSet Controller
- Service Controller
- ServiceAccount Controller
- Volume Controller
- Resource quota Controller
- Disruption Controller
cloud-controller-manager 在 Kubernetes 启用 Cloud Provider 的时候才需要,用来配合云服务提供商的控制,也包括一系列的控制器,如
- Node Controller
- Route Controller
- Node Lifecycle Controller
- Service Controller
kubelet
一旦Pod被调度到对应的宿主机之后,后续要做的事情就是创建这个Pod,并管理这个Pod的生命周期,这里面包括:Pod的增删改查等操作,在K8S里面这部分功能是通过kubelet 这个核心组件来完成的。
对于一个Pod来说,它里面一般会存在多个容器,每个容器里面可以关联不同的镜像,进而运行不同的程序,如此以来:Pod的创建就需要下面的几个核心事件:
感知Pod被创建的命令,并清楚的知道这个Pod创建出来的话需要哪一些具体的信息,而这部分信息的获取是kubelet与k8s交互才能获取到的。
kubelet在获取到这部分数据之后,会根据这些资源信息(包括:cpu、mem、network、image等)操作宿主机来完成资源的分配,网络的构建,并下载镜像到本地,进而将pod启动起来。
这就说明kubelet的功能需要分成两类:
一类:k8s进行交互,获取pod相关的数据,监控当前的Pod变化的事件。
二类:kubelet操作当前宿主机的资源信息,并启动Pod。
Pod启动流程
- 用户通过命令行或yaml文件去创建pod
- apiserver接收到对应请求后,将pod信息写入etcd数据库
- master组件中Controller-Manage通过过apiserver的watch接口发现了pod信息的更新,执行该资源所依赖的拓扑结构整合,整合后将对应的信息交给apiserver,apiserver将pod信息更新写到etcd。
- Scheduler通过apiserver的watch接口更新到pod可以被调度,根据调度算法给pod分配最合适的节点,并将pod和对应节点绑定的信息交给apiserver,apiserver写入etcd。
- apiserver调用node节点上的kubelet,指定pod信息,触发docker run命令创建容器。
- kube-Proxy给pod分配网络资源,将pod的网络和k8s集群的网络连通,之后反馈给pod所在节点上的kubelet, kubelet又将pod的状态信息给apiserver,apiserver又将pod 的状态信息写入etcd
CRI
容器运行时(Container Runtime),运行于Kubernetes(k8s)集群的每个节点中,负责容器的整个生命周期。其中Docker是目前应用最广的。随着容器云的发展,越来越多的容器运行时涌现。为了解决这些容器运行时和Kubernetes的集成问题,在Kubernetes 1.5版本中,社区推出了CRI(Container Runtime Interface,容器运行时接口)以支持更多的容器运行时。
kubelet启动pod的时候是通过CRI进行启动。
CRI是Kubernetes定义的一组gRPC服务。kubelet作为客户端,基于gRPC框架,通过Socket和容器运行时通信。它包括两类服务:镜像服务(Image Service)和运行时服务(Runtime Service)。
- 镜像服务提供下载、检查和删除镜像的远程程序调用。
- 运行时服务包含用于管理容器生命周期,以及与容器交互的调用(exec/attach/port-forward)的远程程序调用。
容器运行时是真正起删除和管理容器的组件。容器运行时可以分为高层和低层的运行时。
高层运行时主要包括Docker,containerd和CRI-0,低层的运行时,包含了runc,kata,以及qVisor。低层运行时kata和gVisor都还处于小规模落地或者实验阶段,其生态成熟度和使用案例都比较欠缺,所以除非有特殊的需求,否则runc几乎是必然的选择。因此在对容器运行时的选择上,主要是聚焦于上层运行时的选择。
Docker内部关于容器运行时功能的核心组件是containerd,后来 containerd也可直接和kubelet通过CRI对接,独立在Kubernetes中使用。相对于Docker 而言,containerd 减少了Docker所需的处理模块Dockerd和Docker-shim,并且对Docker支持的存储驱动进行了优化,因此在容器的创建启动停止和删除,以及对镜像的拉取上,都具有性能上的优势。架构的简化同时也带来了维护的便利。当然Docker也具有很多containerd不具有的功能,例如支持zfs存储驱动,支持对日志的大小和文件限制,在以overlayfs2做存储驱动的情况下,可以通过xfs_quota 来对容器的可写层进行大小限制等。尽管如此,containerd目前也基本上能够满足容器的众多管理需求,所以将它作为运行时的也越来越多。
Docker的多层封装和调用,导致其在可维护性上略逊一筹,增加了线上问题的定位难度;几乎除了重启Docker,我们就毫无他法了。
containerd和CRI-O的方案比起Docker简洁很多。
CNI
Kubernetes网络模型设计的基础原则是:
- 所有的Pod能够不通过NAT就能相互访问。
- 所有的节点能够不通过NAT就能相互访问。
- 容器内看见的IP地址和外部组件看到的容器IP是一样的。
Kubernetes的集群里,IP地址是以Pod为单位进行分配的,每个Pod都拥有一个独立的IP地址。一个Pod内部的所有容器共享一个网络栈,即宿主机上的一个网络命名空间,包括它们的IP地址、网络设备、配置等都是共享的。也就是说,Pod 里面的所有容器能通过localhost:port来连接对方。在Kubernetes中,提供了一个轻量的通用容器网络接口CNI(Container Network Interface),专门用于设置和删除容器的网络连通性。容器运行时通过CNI 调用网络插件来完成容器的网络设置。
CSI
容器运行时存储
除外挂存储卷外,容器启动后,运行时所需文件系统性能直接影响容器性能;
早期的Docker 采用Device Mapper作为容器运行时存储驱动,因为OverlayFS尚未合并进Kernel;
目前Docker 和containerd都默认以OverlayFS作为运行时存储驱动;
OverlayFS目前已经有非常好的性能,与DeviceMapper 相比优20%,与操作主机文件性能几乎一致。
将kubectl apply和replace的区别
kubectl apply如果资源不存在,则使用提供的规范创建资源,如果存在则更新(即修补)。提供的规范apply只需要包含规范的必需部分,在创建资源时,API 将使用其余部分的默认值,而在更新资源时,它将使用其当前值。
将kubectl replace完全取代与所提供的规范中定义的现有资源。
参考:
控制节点上的 CPU 管理策略