kube-scheduler 负责分配Pod到集群内的节点上,它监听kube-apiserver, 查询还未分配 Node 的 Pod, 然后根据调度策略为这些 Pod 分配节点 (更新 Pod 的 NodeName 字段)
调度器需要充分考虑的因素:
公平调度
当资源充分的时候,大家可以公平的一期调度,但是当资源不是那么充分的时候,就需要考虑优先级的问题,高优先级的先调度。
资源高效利用
有的任务需要指定资源消耗,比如说必须最少2个 CPU,就必须调度到剩余 CPU 2个或以上的节点.
有的节点内存消耗多,有的节点CPU消耗多,调度系统在各个节点间调度任务如何能够权衡各种因素,充分利用资源是一个难点。
Qos
任务申请过多的资源是浪费的,此时就需要资源限制来保证任务申请的资源既充分又不会浪费。
affinity 和 anti-affinity
亲和性和反亲和性,有时候有的作业希望跟某些节点相关,比如说计算密集型作业希望被调度到计算密集型节点上。
有的作业在部署时可能希望和已有的应用部署在同一个节点上。
数据本地化
镜像本地缓存
内部负载干扰
https://kubernetes.io/zh-cn/docs/concepts/workloads/pods/disruptions/
deadlines
scheduler 调度分为两个阶段,predicate 和 priority:
Predicates 策略
Predicates plugin 工作原理
Priorities 策略
k8s 资源分配主要由 request 和 limit 来进行限制,request 表示 Pod 在正常运行之外申请的额外资源, limit 表示 Pod 最多能够使用的资源, limit 是由 linux 的cgroup 来做限制的,当超出 limit 之后,Pod 运行的程序将异常。
k8s 的资源分为可压缩资源和不可压缩资源,像 CPU 这种属于可压缩资源, 内存这种资源属于不可压缩资源。
在同一个节点中,当不可压缩资源出现抢占时,将会按照 Pod 配置的 request 和 limit 限制按照一定的策略进行驱逐。
具体的详细信息可以看这篇文章:
Kubernetes 资源分配之 Request 和 Limit 解析
磁盘资源需求
Init Container 的资源需求
具体使用demo
启动 Pod 时指定资源限制:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
memory: 1Gi
cpu: 1
requests:
memory: 256Mi
cpu: 100m
使用默认资源限制:
定义 LimitRange 对象来对 Pod 进行默认资源限制,当定义了 LimitRange 即使在 Pod 定义中没有定义资源限制,也会进行默认的资源限制。
apiVersion: v1
kind: LimitRange
metadata:
name: mem-limit-range
spec:
limits:
- default:
memory: 512Mi
defaultRequest:
memory: 256Mi
type: Container
#################
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
官方文档地址
可以通过 nodeSelector、nodeAffinity、podAffinity 以及 Taints 和 tolerations 等来将 Pod 调度到需要的 Node 上。
也可以通过设置 nodeName 参数,将 Pod 调度到指定 Node 节点上。
nodeSelector 直接指定Pod 部署在哪台节点上。
nodeSelector 实战步骤
https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/assign-pods-nodes/
文档地址:https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/assign-pod-node/
亲和性分为硬亲和性和软亲和性,硬亲和性相当于 nodeSelector 的升级版,你可以定义更多的逻辑决定运行在哪个Node 上或者和哪些 Pod 运行在同一个 Node 上,软亲和是尽量满足对应的控制逻辑。
节点亲和性主要由两种:
节点亲和性 yaml 说明:
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- antarctica-east1
- antarctica-west1
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: with-node-affinity
image: registry.k8s.io/pause:2.0
.spec.affinity.nodeAffinity
字段中指定节点亲和性topology.kubernetes.io/zone
的标签, 并且该标签的取值必须为 antarctica-east1
或 antarctica-west1
。another-node-label-key
且取值为 another-node-label-value
的标签。Pod 亲和性和节点亲和性一样也有两种类型的亲和性,与节点亲和性不同的是 Pod 亲和性中多了
topologyKey`标签来表达域的概念,是必填字段。
Pod 亲和性实战
部署一个带有 redis 缓存的 server, 该 server 需要在三个不同的节点上负载,redis 缓存要和 server 在同一个节点上,但是 三个 redis 也不能部署在一个几点上。
也就是说每个节点需要部署一个 server 和一个 redis:
将 redis 部署在三台不同节点上
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
selector:
matchLabels:
app: store
replicas: 3
template:
metadata:
labels:
app: store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: redis-server
image: redis:3.2-alpine
将 server 部署在不同节点上,并且和 redis 所在节点保持一致
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-store
replicas: 3
template:
metadata:
labels:
app: web-store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-store
topologyKey: "kubernetes.io/hostname"
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-app
image: nginx:1.16-alpine
文档地址:https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/taint-and-toleration/
Taints 和 Tolerations 用于保证 Pod 不被调度到不合适的 Node 上,其中 Taint 应用于 Node 上,而 Toleration 则应用于 Pod 上。
目前支持的 Taint 类型:
controller manager 是整个集群的大脑,它会监听所有的API资源对象,它主要分为 informer 和 Lister 两个对象,Lister 会获取当前资源对象的状况,informer 会监听资源对象,当资源对象变更时,会有对应的事件队列去处理,相当于一个消息通知机制的接口。
Informer 的内部机制
Informer 会和 API Server 产生一个长连接,去监听并获取所有资源对象的最新信息。
API Server 返回的是一个 json 字符串,Informer 在一开始 Reflector 需要利用 Go 的反射机制,根据tag 将字符串中转换为 Go 对象。
controller manager 是controller 的集合,每一种 controller 负责监听不同的对象,下边以部署一个 deployment 为例说明其中的不同 controller 的分工合作。
同时 kubelet 也会监听节点上的 Pod 信息,并且不断上报信息至 API Server。
可以发现在对一个资源对象进行处理时,会经过许多的Controller 对象,这样解耦了整个冗长的部署流程,并且更加符合声明式的编程,每个Controller 监听对应对象的最新状态,当不符合期望时就发生变更,达到最终一致性的状态。
每个节点上都运行一个 kubelet 服务进程,默认监听 10250 端口。
节点管理
节点管理主要是节点自注册和节点状态更新:
--register-node
来确定是否向 API Server 注册自己Pod 管理
kubelet 的核心功能就是管理 Pod 的整个生命周期,然后上报节点与 Pod 信息至 API Server。
kubelet 的整体架构主要分为四层:
第一层是对外API
第二层是各种管理器
负责 Pod 在节点上的稳定运行以及资源使用状况的监控,其中最终要的就是 syncLoop 和 PodWorker, syncLoop 负责监听对 Pod 对象的各种操作,然后分配对应的 PodWorker 异步处理。
第三层是CRI 容器运行时接口
负责和节点上运行的容器进程交互,启动 Pod 容器进程。
第四层是容器运行层
节点上运行的容器进程,主要分为两大类,一类的是 docker 的进程一类是k8s 自身的 containerd 进程,k8s 所谓的废弃 docker 并不是说停止使用整个 docker 的进程,而是废弃 docker shim 的使用。
然后推出自己的容器运行时标准 containerd,不过后来 docker 也兼容了 k8s 提出的容器运行时标准,否则 docker 将被整个弃用。
核心功能中最重要的是 syncLoop 和 PLEG 两大组件:
接收API Server 下发的 Pod 操作命令
syncLoop 接收到命令后,会分配对应的 worker 进行处理,在 worker 中会启动对应的 Pod, 并且执行 Pod 该有的 actions, 在一系列 manager 的配合下调用 CRI 执行容器操作。
PLEG(Pod Lifecycle Event Generator) PLEG 是 kubelet 的核心模块,PLEG 会一直调用 container runtime 获取本节点 containers/sandboxes 的信息,并与自身维护的 pods cache 信息进行对比,生成对应的 PodLifecycleEvent,然后输出到 eventChannel 中,通过 eventChannel 发送到 kubelet syncLoop 进行消费,然后由 kubelet syncPod 来触发 pod 同步处理过程,最终达到用户的期望状态。
不断上报 Pods 的信息至 API Server
同时 PLEG 也会不断上报 Pods 的最新信息至 syncLoop,然后 syncLoop 再上报至 API Server.
在 kubelet 启动Pod 的流程中有一个非常重要的环节就是和 CRI 的交互,CRI 会启动一个Sandbox 的容器,该容器将会打通与CNI的网络通信,为 Pod 在节点上提供一个稳定运行的环境,可以理解为给 Pod 在节点上的运行提供了一个沙箱环境。
K8s 与 CRI(Container Runtime Interface)、contaier 之间的关系,可以参考下这篇文章:
https://zhuanlan.zhihu.com/p/102897620
容器运行时(Container Runtime),运行于 k8s 集群的每个节点,负责容器的整个生命周期。
k8s 最开始的容器运行时是 Docker, 后来为了适用更多的容器运行时,k8s 提出了 CRI(Container Runtime Interface)容器运行时接口,kubelet 通过 CRI 来与容器运行时通信。
CRI 是 k8s 定义的一组gRPC 服务,kubelet 作为客户端,基于 gRPC 框架,通过Socket 和容器运行时通信。
它包括两类服务:
Dockershim, containerd 和 CRI-O 都是遵循 CRI 的容器运行时,称为高层级运行时。
OCI (Open Container Initiative, 开放容器计划)定义了创建容器的格式和运行时的开源行业标准,包括镜像规范和运行时规范:
如何为新容器设置命名空间(namespaces)和控制组(cgroups),以及挂载根文件系统等等,被称为低层级运行时。
容器运行时是真正启删和管理容器的组件,分为高层级运行时和低层级运行时,高层级运行时主要包括 Docker, containerd 和 CRI-O,低层级运行时包括 runc, kata以及 gVisor。
目前底层级运行时尚未成熟,还在实验阶段,因此更多的是使用高层级运行时。
Docker 关于容器运行时操作的核心也是 containerd, 但是 Docker 在其之上又封装了多层,比如 dockershim, docker-cli,这些封装对于 k8s 来说是冗余的,导致其在可维护性上略逊一筹,增加了线上问题的定位难度,当容器运行时出现问题时,除了重启 Docker 别无他法,因此 k8s 定义了CRI 企图废弃 Docker ,让开源的容器运行时去遵守 k8s 的规范:
如上图中所示,如今的 k8s 只保留了containerd, 而废弃了 Docker。
下图是containerd 和 CRI-O 的方案,可以发现比 Docker 简洁了很多,越往下 k8s 废弃越多的冗余部分:
目前成熟的容器运行时是上边这些的高层级运行时,一旦低层级运行时的开源服务成熟,将会更加简洁。
k8s 网络模型设计的基础原则是:
k8s 集群中的 IP 地址以 Pod 为单位分配,每个 Pod 拥有一个独立的 IP 地址, Pod 内的所有容器共享一个网络栈,可以通过 localhost:port 来连接对方。
CNI 插件分类和常见插件
参考链接:https://github.com/containernetworking/plugins
CRI 会从 CNI 配置目录中读取JSON 格式的配置文件,文件后缀为".conf", “.conflist”, “.json”。
如果配置目录中包含多个文件,一般情况下会以名字排序,读取第一个,获取其中指定的插件名称和配置参数。
CNI 的运行机制
关于容器网络管理,容器运行时一般需要配置两个参数 --cni-bin-dir 和 --cni-conf-dir。
Flannel