forked from gosoon/source-code-reading-notes
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch_plus_index.json
More file actions
1 lines (1 loc) · 258 KB
/
search_plus_index.json
File metadata and controls
1 lines (1 loc) · 258 KB
1
{"./":{"url":"./","title":"Introduction","keywords":"","body":"source-code-reading-notes 本项目主要记录工作过程中阅读过的一些开源项目的源码,并加以自己的分析,目前主要专注 k8s 云原生实践,包括但不限于 docker、kubernetes、promethus、istio、knative、service mesh、serverless 等。 在线阅读:https://blog.tianfeiyu.com/source-code-reading-notes/ 本项目会不定期更新,最新发表的文章会及时推送到公众号,欢迎关注: Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-08 17:54:45 "},"kubernetes/":{"url":"kubernetes/","title":"kubernetes","keywords":"","body":"本章记录 kubernetes 源码分析相关的文章,文章主要基于 kubernetes v1.16 版本,文中如有不当之处望指正。 Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/statefulset_controller.html":{"url":"kubernetes/statefulset_controller.html","title":"statefulset controller 源码分析","keywords":"","body":"Statefulset 的基本功能 statefulset 旨在与有状态的应用及分布式系统一起使用,statefulset 中的每个 pod 拥有一个唯一的身份标识,并且所有 pod 名都是按照 {0..N-1} 的顺序进行编号。本文会主要分析 statefulset controller 的设计与实现,在分析源码前先介绍一下 statefulset 的基本使用。 创建 对于一个拥有 N 个副本的 statefulset,pod 是按照 {0..N-1}的序号顺序创建的,并且会等待前一个 pod 变为 Running & Ready 后才会启动下一个 pod。 $ kubectl create -f sts.yaml $ kubectl get pod -o wide -w NAME READY STATUS RESTARTS AGE IP NODE web-0 0/1 ContainerCreating 0 20s minikube web-0 1/1 Running 0 3m1s 10.1.0.8 minikube web-1 0/1 Pending 0 0s web-1 0/1 ContainerCreating 0 2s minikube web-1 1/1 Running 0 4s 10.1.0.9 minikube 扩容 statefulset 扩容时 pod 也是顺序创建的,编号与前面的 pod 相接。 $ kubectl scale sts web --replicas=4 statefulset.apps/web scaled $ kubectl get pod -o wide -w ...... web-2 0/1 Pending 0 0s web-2 0/1 ContainerCreating 0 1s minikube web-2 1/1 Running 0 4s 10.1.0.10 minikube web-3 0/1 Pending 0 0s web-3 0/1 ContainerCreating 0 1s minikube web-3 1/1 Running 0 4s 10.1.0.11 minikube 缩容 缩容时控制器会按照与 pod 序号索引相反的顺序每次删除一个 pod,在删除下一个 pod 前会等待上一个被完全删除。 $ kubectl scale sts web --replicas=2 $ kubectl get pod -o wide -w ...... web-3 1/1 Terminating 0 8m25s 10.1.0.11 minikube web-3 0/1 Terminating 0 8m27s minikube web-2 1/1 Terminating 0 8m31s 10.1.0.10 minikube web-2 0/1 Terminating 0 8m33s 10.1.0.10 minikube 更新 更新策略由 statefulset 中的 spec.updateStrategy.type 字段决定,可以指定为 OnDelete 或者 RollingUpdate , 默认的更新策略为 RollingUpdate。当使用RollingUpdate 更新策略更新所有 pod 时采用与序号索引相反的顺序进行更新,即最先删除序号最大的 pod 并根据更新策略中的 partition 参数来进行分段更新,控制器会更新所有序号大于或等于 partition 的 pod,等该区间内的 pod 更新完成后需要再次设定 partition 的值以此来更新剩余的 pod,最终 partition 被设置为 0 时代表更新完成了所有的 pod。在更新过程中,如果一个序号小于 partition 的 pod 被删除或者终止,controller 依然会使用更新前的配置重新创建。 // 使用 RollingUpdate 策略更新 $ kubectl patch statefulset web --type='json' -p='[{\"op\": \"replace\", \"path\": \"/spec/template/spec/containers/0/image\", \"value\":\"nginx:1.16\"}]' statefulset.apps/web patched $ kubectl rollout status sts/web Waiting for 1 pods to be ready... Waiting for partitioned roll out to finish: 1 out of 2 new pods have been updated... Waiting for 1 pods to be ready... partitioned roll out complete: 2 new pods have been updated... 如果 statefulset 的 .spec.updateStrategy.type 字段被设置为 OnDelete,在更新 statefulset 时,statefulset controller 将不会自动更新其 pod。你必须手动删除 pod,此时 statefulset controller 在重新创建 pod 时,使用修改过的 .spec.template 的内容创建新 pod。 // 使用 OnDelete 方式更新 $ kubectl patch statefulset nginx --type='json' -p='[{\"op\": \"replace\", \"path\": \"/spec/template/spec/containers/0/image\", \"value\":\"nginx:1.9\"}]' // 删除 web-1 $ kubectl delete pod web-1 // 查看 web-0 与 web-1 的镜像版本,此时发现 web-1 已经变为最新版本 nginx:1.9 了 $ kubectl get pod -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}{\"\\t\"}{.spec.containers[0].image}{\"\\n\"}{end}' web-0 nginx:1.16 web-1 nginx:1.9 使用滚动更新策略时你必须以某种策略不段更新 partition 值来进行升级,类似于金丝雀部署方式,升级对于 pod 名称来说是逆序。使用非滚动更新方式式,需要手动删除对应的 pod,升级可以是无序的。 回滚 statefulset 和 deployment 一样也支持回滚操作,statefulset 也保存了历史版本,和 deployment 一样利用.spec.revisionHistoryLimit 字段设置保存多少个历史版本,但 statefulset 的回滚并不是自动进行的,回滚操作也仅仅是进行了一次发布更新,和发布更新的策略一样,更新 statefulset 后需要按照对应的策略手动删除 pod 或者修改 partition 字段以达到回滚 pod 的目的。 // 查看 sts 的历史版本 $ kubectl rollout history statefulset web statefulset.apps/web REVISION 0 0 5 6 $ kubectl get controllerrevision NAME CONTROLLER REVISION AGE web-6c4c79564f statefulset.apps/web 6 11m web-c47b9997f statefulset.apps/web 5 4h13m // 回滚至最近的一个版本 $ kubectl rollout undo statefulset web --to-revision=5 因为 statefulset 的使用对象是有状态服务,大部分有状态副本集都会用到持久存储,statefulset 下的每个 pod 正常情况下都会关联一个 pv 对象,对 statefulset 对象回滚非常容易,但其使用的 pv 中保存的数据无法回滚,所以在生产环境中进行回滚时需要谨慎操作,statefulset、pod、pvc 和 pv 关系图如下所示: 删除 statefulset 同时支持级联和非级联删除。使用非级联方式删除 statefulset 时,statefulset 的 pod 不会被删除。使用级联删除时,statefulset 和它关联的 pod 都会被删除。对于级联与非级联删除,在删除时需要指定删除选项(orphan、background 或者 foreground)进行区分。 // 1、非级联删除 $ kubectl delete statefulset web --cascade=false // 删除 sts 后 pod 依然处于运行中 $ kubectl get pod NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 4m38s web-1 1/1 Running 0 17m // 重新创建 sts 后,会再次关联所有的 pod $ kubectl create -f sts.yaml $ kubectl get sts NAME READY AGE web 2/2 28s 在级联删除 statefulset 时,会将所有的 pod 同时删掉,statefulset 控制器会首先进行一个类似缩容的操作,pod 按照和他们序号索引相反的顺序每次终止一个。在终止一个 pod 前,statefulset 控制器会等待 pod 后继者被完全终止。 // 2、级联删除 $ kubectl delete statefulset web $ kubectl get pod -o wide -w ...... web-0 1/1 Terminating 0 17m 10.1.0.18 minikube web-1 1/1 Terminating 0 36m 10.1.0.15 minikube web-1 0/1 Terminating 0 36m 10.1.0.15 minikube web-0 0/1 Terminating 0 17m 10.1.0.18 minikube Pod 管理策略 statefulset 的默认管理策略是 OrderedReady,该策略遵循上文展示的顺序性保证。statefulset 还有另外一种管理策略 Parallel,Parallel 管理策略告诉 statefulset 控制器并行的终止所有 pod,在启动或终止另一个 pod 前,不必等待这些 pod 变成 Running & Ready 或者完全终止状态,但是 Parallel 仅仅支持在 OnDelete 策略下生效,下文会在源码中具体分析。 StatefulSetController 源码分析 kubernetes 版本:v1.16 startStatefulSetController 是 statefulSetController 的启动方法,其中调用 NewStatefulSetController 进行初始化 controller 对象然后调用 Run 方法启动 controller。其中 ConcurrentStatefulSetSyncs 默认值为 5。 k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:55 func startStatefulSetController(ctx ControllerContext) (http.Handler, bool, error) { if !ctx.AvailableResources[schema.GroupVersionResource{Group: \"apps\", Version: \"v1\", Resource: \"statefulsets\"}] { return nil, false, nil } go statefulset.NewStatefulSetController( ctx.InformerFactory.Core().V1().Pods(), ctx.InformerFactory.Apps().V1().StatefulSets(), ctx.InformerFactory.Core().V1().PersistentVolumeClaims(), ctx.InformerFactory.Apps().V1().ControllerRevisions(), ctx.ClientBuilder.ClientOrDie(\"statefulset-controller\"), ).Run(int(ctx.ComponentConfig.StatefulSetController.ConcurrentStatefulSetSyncs), ctx.Stop) return nil, true, nil } 当 controller 启动后会通过 informer 同步 cache 并监听 pod 和 statefulset 对象的变更事件,informer 的处理流程此处不再详细讲解,最后会执行 sync 方法,sync 方法是每个 controller 的核心方法,下面直接看 statefulset controller 的 sync 方法。 sync sync 方法的主要逻辑为: 1、根据 ns/name 获取 sts 对象; 2、获取 sts 的 selector; 3、调用 ssc.adoptOrphanRevisions 检查是否有孤儿 controllerrevisions 对象,若有且能匹配 selector 的则添加 ownerReferences 进行关联,已关联但 label 不匹配的则进行释放; 4、调用 ssc.getPodsForStatefulSet 通过 selector 获取 sts 关联的 pod,若有孤儿 pod 的 label 与 sts 的能匹配则进行关联,若已关联的 pod label 有变化则解除与 sts 的关联关系; 5、最后调用 ssc.syncStatefulSet 执行真正的 sync 操作; k8s.io/kubernetes/pkg/controller/statefulset/stateful_set.go:408 func (ssc *StatefulSetController) sync(key string) error { ...... namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } // 1、获取 sts 对象 set, err := ssc.setLister.StatefulSets(namespace).Get(name) ...... selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector) ...... // 2、关联以及释放 sts 的 controllerrevisions if err := ssc.adoptOrphanRevisions(set); err != nil { return err } // 3、获取 sts 所关联的 pod pods, err := ssc.getPodsForStatefulSet(set, selector) if err != nil { return err } return ssc.syncStatefulSet(set, pods) } syncStatefulSet 在 syncStatefulSet 中仅仅是调用了 ssc.control.UpdateStatefulSet 方法进行处理。ssc.control.UpdateStatefulSet 会调用 defaultStatefulSetControl 的 UpdateStatefulSet 方法,defaultStatefulSetControl 是 statefulset controller 中另外一个对象,主要负责处理 statefulset 的更新。 k8s.io/kubernetes/pkg/controller/statefulset/stateful_set.go:448 func (ssc *StatefulSetController) syncStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error { ...... if err := ssc.control.UpdateStatefulSet(set.DeepCopy(), pods); err != nil { return err } ...... return nil } UpdateStatefulSet 方法的主要逻辑如下所示: 1、获取历史 revisions; 2、计算 currentRevision 和 updateRevision,若 sts 处于更新过程中则 currentRevision 和 updateRevision 值不同; 3、调用 ssc.updateStatefulSet 执行实际的 sync 操作; 4、调用 ssc.updateStatefulSetStatus 更新 status subResource; 5、根据 sts 的 spec.revisionHistoryLimit字段清理过期的 controllerrevision; 在基本操作的回滚阶段提到了过,sts 通过 controllerrevision 保存历史版本,类似于 deployment 的 replicaset,与 replicaset 不同的是 controllerrevision 仅用于回滚阶段,在 sts 的滚动升级过程中是通过 currentRevision 和 updateRevision来j进行控制并不会用到 controllerrevision。 k8s.io/kubernetes/pkg/controller/statefulset/stateful_set_control.go:75 func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error { // 1、获取历史 revisions revisions, err := ssc.ListRevisions(set) if err != nil { return err } history.SortControllerRevisions(revisions) // 2、计算 currentRevision 和 updateRevision currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions) if err != nil { return err } // 3、执行实际的 sync 操作 status, err := ssc.updateStatefulSet(set, currentRevision, updateRevision, collisionCount, pods) if err != nil { return err } // 4、更新 sts 状态 err = ssc.updateStatefulSetStatus(set, status) if err != nil { return err } ...... // 5、清理过期的历史版本 return ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision) } updateStatefulSet updateStatefulSet 是 sync 操作中的核心方法,对于 statefulset 的创建、扩缩容、更新、删除等操作都会在这个方法中完成,以下是其主要逻辑: 1、分别获取 currentRevision 和 updateRevision 对应的的 statefulset object; 2、构建 status 对象; 3、将 statefulset 的 pods 按 ord(ord 为 pod name 中的序号)的值分到 replicas 和 condemned 两个数组中,0 = Spec.Replicas 的放到 condemned 组,replicas 组代表可用的 pod,condemned 组是需要删除的 pod; 4、找出 replicas 和 condemned 组中的 unhealthy pod,healthy pod 指 running & ready 并且不处于删除状态; 5、判断 sts 是否处于删除状态; 6、遍历 replicas 数组,确保 replicas 数组中的容器处于 running & ready状态,其中处于 failed 状态的容器删除重建,未创建的容器则直接创建,最后检查 pod 的信息是否与 statefulset 的匹配,若不匹配则更新 pod 的状态。在此过程中每一步操作都会检查 monotonic 的值,即 sts 是否设置了 Parallel 参数,若设置了则循环处理 replicas 中的所有 pod,否则每次处理一个 pod,剩余 pod 则在下一个 syncLoop 继续进行处理; 7、按 pod 名称逆序删除 condemned 数组中的 pod,删除前也要确保 pod 处于 running & ready状态,在此过程中也会检查 monotonic 的值,以此来判断是顺序删除还是在下一个 syncLoop 中继续进行处理; 8、判断 sts 的更新策略 .Spec.UpdateStrategy.Type,若为 OnDelete 则直接返回; 9、此时更新策略为 RollingUpdate,更新序号大于等于 .Spec.UpdateStrategy.RollingUpdate.Partition 的 pod,在 RollingUpdate 时,并不会关注 monotonic 的值,都是顺序进行处理且等待当前 pod 删除成功后才继续删除小于上一个 pod 序号的 pod,所以 Parallel 的策略在滚动更新时无法使用。 updateStatefulSet 这个方法中包含了 statefulset 的创建、删除、扩若容、更新等操作,在源码层面对于各个功能无法看出明显的界定,没有 deployment sync 方法中写的那么清晰,下面还是按 statefulset 的功能再分析一下具体的操作: 创建:在创建 sts 后,sts 对象已被保存至 etcd 中,此时 sync 操作仅仅是创建出需要的 pod,即执行到第 6 步就会结束; 扩缩容:对于扩若容操作仅仅是创建或者删除对应的 pod,在操作前也会判断所有 pod 是否处于 running & ready状态,然后进行对应的创建/删除操作,在上面的步骤中也会执行到第 6 步就结束了; 更新:可以看出在第六步之后的所有操作就是与更新相关的了,所以更新操作会执行完整个方法,在更新过程中通过 pod 的 currentRevision 和 updateRevision 来计算 currentReplicas、updatedReplicas 的值,最终完成所有 pod 的更新; 删除:删除操作就比较明显了,会止于第五步,但是在此之前检查 pod 状态以及分组的操作确实是多余的; k8s.io/kubernetes/pkg/controller/statefulset/stateful_set_control.go:255 func (ssc *defaultStatefulSetControl) updateStatefulSet(......) (*apps.StatefulSetStatus, error) { // 1、分别获取 currentRevision 和 updateRevision 对应的的 statefulset object currentSet, err := ApplyRevision(set, currentRevision) if err != nil { return nil, err } updateSet, err := ApplyRevision(set, updateRevision) if err != nil { return nil, err } // 2、计算 status status := apps.StatefulSetStatus{} status.ObservedGeneration = set.Generation status.CurrentRevision = currentRevision.Name status.UpdateRevision = updateRevision.Name status.CollisionCount = new(int32) *status.CollisionCount = collisionCount // 3、将 statefulset 的 pods 按 ord(ord 为 pod name 中的序数)的值 // 分到 replicas 和 condemned 两个数组中 replicaCount := int(*set.Spec.Replicas) replicas := make([]*v1.Pod, replicaCount) condemned := make([]*v1.Pod, 0, len(pods)) unhealthy := 0 firstUnhealthyOrdinal := math.MaxInt32 var firstUnhealthyPod *v1.Pod // 4、计算 status 字段中的值,将 pod 分配到 replicas和condemned两个数组中 for i := range pods { status.Replicas++ if isRunningAndReady(pods[i]) { status.ReadyReplicas++ } if isCreated(pods[i]) && !isTerminating(pods[i]) { if getPodRevision(pods[i]) == currentRevision.Name { status.CurrentReplicas++ } if getPodRevision(pods[i]) == updateRevision.Name { status.UpdatedReplicas++ } } if ord := getOrdinal(pods[i]); 0 = replicaCount { condemned = append(condemned, pods[i]) } } // 5、检查 replicas数组中 [0,set.Spec.Replicas) 下标是否有缺失的 pod,若有缺失的则创建对应的 pod object // 在 newVersionedStatefulSetPod 中会判断是使用 currentSet 还是 updateSet 来创建 for ord := 0; ord = 0; target-- { // 18、如果pod正在删除,检查 Spec.PodManagementPolicy 的值,如果为Parallel, // 循环处理下一个pod 否则直接退出 if isTerminating(condemned[target]) { ...... if monotonic { return &status, nil } continue } // 19、不满足以下条件说明该 pod 是更新前创建的,正处于创建中 if !isRunningAndReady(condemned[target]) && monotonic && condemned[target] != firstUnhealthyPod { ...... return &status, nil } // 20、否则直接删除该 pod if err := ssc.podControl.DeleteStatefulPod(set, condemned[target]); err != nil { return &status, err } if getPodRevision(condemned[target]) == currentRevision.Name { status.CurrentReplicas-- } if getPodRevision(condemned[target]) == updateRevision.Name { status.UpdatedReplicas-- } // 21、如果为 OrderedReady 方式则返回否则继续处理下一个 pod if monotonic { return &status, nil } } // 22、对于 OnDelete 策略直接返回 if set.Spec.UpdateStrategy.Type == apps.OnDeleteStatefulSetStrategyType { return &status, nil } // 23、若为 RollingUpdate 策略,则倒序处理 replicas数组中下标大于等于 // Spec.UpdateStrategy.RollingUpdate.Partition 的 pod updateMin := 0 if set.Spec.UpdateStrategy.RollingUpdate != nil { updateMin = int(*set.Spec.UpdateStrategy.RollingUpdate.Partition) } for target := len(replicas) - 1; target >= updateMin; target-- { // 24、如果Pod的Revision 不等于 updateRevision,且 pod 没有处于删除状态则直接删除 pod if getPodRevision(replicas[target]) != updateRevision.Name && !isTerminating(replicas[target]) { ...... err := ssc.podControl.DeleteStatefulPod(set, replicas[target]) status.CurrentReplicas-- return &status, err } // 25、如果 pod 非 healthy 状态直接返回 if !isHealthy(replicas[target]) { return &status, nil } } return &status, nil } 总结 本文分析了 statefulset controller 的主要功能,statefulset 在设计上有很多功能与 deployment 是类似的,但其主要是用来部署有状态应用的,statefulset 中的 pod 名称存在顺序性和唯一性,同时每个 pod 都使用了 pv 和 pvc 来存储状态,在创建、删除、更新操作中都会按照 pod 的顺序进行。 参考: https://github.com/kubernetes/kubernetes/issues/78007 https://github.com/kubernetes/kubernetes/issues/67250 https://www.cnblogs.com/linuxk/p/9767736.html Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-11 20:48:12 "},"kubernetes/deployment_controller.html":{"url":"kubernetes/deployment_controller.html","title":"deployment controller 源码分析","keywords":"","body":"在前面的文章中已经分析过 kubernetes 中多个组件的源码了,本章会继续解读 kube-controller-manager 源码,kube-controller-manager 中有数十个 controller,本文会分析最常用到的 deployment controller。 deployment 的功能 deployment 是 kubernetes 中用来部署无状态应用的一个对象,也是最常用的一种对象。 deployment、replicaSet 和 pod 之间的关系 deployment 的本质是控制 replicaSet,replicaSet 会控制 pod,然后由 controller 驱动各个对象达到期望状态。 DeploymentController 是 Deployment 资源的控制器,其通过 DeploymentInformer、ReplicaSetInformer、PodInformer 监听三种资源,当三种资源变化时会触发 DeploymentController 中的 syncLoop 操作。 deployment 的基本功能 下面通过命令行操作展示一下 deployment 的基本功能。 以下是 deployment 的一个示例文件: apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: progressDeadlineSeconds: 600 // 执行操作的超时时间 replicas: 20 revisionHistoryLimit: 10 // 保存的历史版本数量 selector: matchLabels: app: nginx-deployment strategy: rollingUpdate: maxSurge: 25% // 升级过程中最多可以比原先设置多出的 pod 数量 maxUnavailable: 25% // 升级过程中最多有多少个 pod 处于无法提供服务的状态 type: RollingUpdate // 更新策略 template: metadata: labels: app: nginx-deployment spec: containers: - name: nginx-deployment image: nginx:1.9 imagePullPolicy: IfNotPresent ports: - containerPort: 80 创建 $ kubectl create -f nginx-dep.yaml --record $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deployment 20/20 20 20 22h $ kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deployment-68b649bd8b 20 20 20 22h 滚动更新 $ kubectl set image deploy/nginx-deployment nginx-deployment=nginx:1.9.3 $ kubectl rollout status deployment/nginx-deployment 回滚 // 查看历史版本 $ kubectl rollout history deployment/nginx-deployment deployment.extensions/nginx-deployment REVISION CHANGE-CAUSE 4 5 // 指定版本回滚 $ kubectl rollout undo deployment/nginx-deployment --to-revision=2 扩缩容 $ kubectl scale deployment nginx-deployment --replicas 10 deployment.extensions/nginx-deployment scaled 暂停与恢复 $ kubectl rollout pause deployment/nginx-deployment $ kubectl rollout resume deploy nginx-deployment 删除 $ kubectl delete deployment nginx-deployment 以上是 deployment 的几个常用操作,下面会结合源码分析这几个操作都是如何实现的。 deployment controller 源码分析 kubernetes 版本:v1.16 在控制器模式下,每次操作对象都会触发一次事件,然后 controller 会进行一次 syncLoop 操作,controller 是通过 informer 监听事件以及进行 ListWatch 操作的,关于 informer 的基础知识可以参考以前写的文章。 deployment controller 启动流程 kube-controller-manager 中所有 controller 的启动都是在 Run 方法中完成初始化并启动的。在 Run 中会调用 run 函数,run 函数的主要流程有: 1、调用 NewControllerInitializers 初始化所有 controller 2、调用 StartControllers 启动所有 controller k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go:158 func Run(c *config.CompletedConfig, stopCh NewControllerInitializers 中定义了所有的 controller 以及 start controller 对应的方法。deployment controller 对应的启动方法是 startDeploymentController。 k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go:373 func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc { controllers := map[string]InitFunc{} ...... controllers[\"deployment\"] = startDeploymentController ...... } 在startDeploymentController 中对 deploymentController 进行了初始化,并执行 dc.Run() 方法启动了 controller。 k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:82 func startDeploymentController(ctx ControllerContext) (http.Handler, bool, error) { ...... // 初始化 controller dc, err := deployment.NewDeploymentController( ctx.InformerFactory.Apps().V1().Deployments(), ctx.InformerFactory.Apps().V1().ReplicaSets(), ctx.InformerFactory.Core().V1().Pods(), ctx.ClientBuilder.ClientOrDie(\"deployment-controller\"), ) ...... // 启动 controller go dc.Run(int(ctx.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs), ctx.Stop) return nil, true, nil } ctx.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs 指定了 deployment controller 中工作的 goroutine 数量,默认值为 5,即会启动五个 goroutine 从 workqueue 中取出 object 并进行 sync 操作,该参数的默认值定义在 k8s.io/kubernetes/pkg/controller/deployment/config/v1alpha1/defaults.go 中。 dc.Run 方法会执行 ListWatch 操作并根据对应的事件执行 syncLoop。 k8s.io/kubernetes/pkg/controller/deployment/deployment_controller.go:148 func (dc *DeploymentController) Run(workers int, stopCh dc.worker 会调用 syncHandler 进行 sync 操作。 func (dc *DeploymentController) worker() { for dc.processNextWorkItem() { } } func (dc *DeploymentController) processNextWorkItem() bool { key, quit := dc.queue.Get() if quit { return false } defer dc.queue.Done(key) // 若 workQueue 中有任务则进行处理 err := dc.syncHandler(key.(string)) dc.handleErr(err, key) return true } syncHandler 是 controller 的核心逻辑,下面会进行详细说明。至此,对于 deployment controller 的启动流程已经分析完,再来看一下 deployment controller 启动过程中的整个调用链,如下所示: Run() --> run() --> NewControllerInitializers() --> StartControllers() --> startDeploymentController() --> deployment.NewDeploymentController() --> deployment.Run() --> deployment.syncDeployment() deployment controller 在初始化时指定了 dc.syncHandler = dc.syncDeployment,所以该函数名为 syncDeployment,本文开头介绍 deployment 中的基本操作都是在 syncDeployment 中完成的。 syncDeployment 的主要流程如下所示: 1、调用 getReplicaSetsForDeployment 获取集群中与 Deployment 相关的 ReplicaSet,若发现匹配但没有关联 deployment 的 rs 则通过设置 ownerReferences 字段与 deployment 关联,已关联但不匹配的则删除对应的 ownerReferences; 2、调用 getPodMapForDeployment 获取当前 Deployment 对象关联的 pod,并根据 rs.UID 对上述 pod 进行分类; 3、通过判断 deployment 的 DeletionTimestamp 字段确认是否为删除操作; 4、执行 checkPausedConditions检查 deployment 是否为pause状态并添加合适的condition; 5、调用 getRollbackTo 函数检查 Deployment 是否有Annotations:\"deprecated.deployment.rollback.to\"字段,如果有,调用 dc.rollback 方法执行 rollback 操作; 6、调用 dc.isScalingEvent 方法检查是否处于 scaling 状态中; 7、最后检查是否为更新操作,并根据更新策略 Recreate 或 RollingUpdate 来执行对应的操作; k8s.io/kubernetes/pkg/controller/deployment/deployment_controller.go:562 func (dc *DeploymentController) syncDeployment(key string) error { ...... namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } // 1、从 informer cache 中获取 deployment 对象 deployment, err := dc.dLister.Deployments(namespace).Get(name) if errors.IsNotFound(err) { ...... } ...... d := deployment.DeepCopy() // 2、判断 selecor 是否为空 everything := metav1.LabelSelector{} if reflect.DeepEqual(d.Spec.Selector, &everything) { ...... return nil } // 3、获取 deployment 对应的所有 rs,通过 LabelSelector 进行匹配 rsList, err := dc.getReplicaSetsForDeployment(d) if err != nil { return err } // 4、获取当前 Deployment 对象关联的 pod,并根据 rs.UID 对 pod 进行分类 podMap, err := dc.getPodMapForDeployment(d, rsList) if err != nil { return err } // 5、如果该 deployment 处于删除状态,则更新其 status if d.DeletionTimestamp != nil { return dc.syncStatusOnly(d, rsList) } // 6、检查是否处于 pause 状态 if err = dc.checkPausedConditions(d); err != nil { return err } if d.Spec.Paused { return dc.sync(d, rsList) } // 7、检查是否为回滚操作 if getRollbackTo(d) != nil { return dc.rollback(d, rsList) } // 8、检查 deployment 是否处于 scale 状态 scalingEvent, err := dc.isScalingEvent(d, rsList) if err != nil { return err } if scalingEvent { return dc.sync(d, rsList) } // 9、更新操作 switch d.Spec.Strategy.Type { case apps.RecreateDeploymentStrategyType: return dc.rolloutRecreate(d, rsList, podMap) case apps.RollingUpdateDeploymentStrategyType: return dc.rolloutRolling(d, rsList) } return fmt.Errorf(\"unexpected deployment strategy type: %s\", d.Spec.Strategy.Type) } 可以看出对于 deployment 的删除、暂停恢复、扩缩容以及更新操作都是在 syncDeployment 方法中进行处理的,最终是通过调用 syncStatusOnly、sync、rollback、rolloutRecreate、rolloutRolling 这几个方法来处理的,其中 syncStatusOnly 和 sync 都是更新 Deployment 的 Status,rollback 是用来回滚的,rolloutRecreate 和 rolloutRolling 是根据不同的更新策略来更新 Deployment 的,下面就来看看这些操作的具体实现。 从 syncDeployment 中也可知以上几个操作的优先级为: delete > pause > rollback > scale > rollout 举个例子,当在 rollout 操作时可以执行 pause 操作,在 pause 状态时也可直接执行删除操作。 删除 syncDeployment 中首先处理的是删除操作,删除操作是由客户端发起的,首先会在对象的 metadata 中设置 DeletionTimestamp 字段。 func (dc *DeploymentController) syncDeployment(key string) error { ...... if d.DeletionTimestamp != nil { return dc.syncStatusOnly(d, rsList) } ...... } 当 controller 检查到该对象有了 DeletionTimestamp 字段时会调用 dc.syncStatusOnly 执行对应的删除逻辑,该方法首先获取 newRS 以及所有的 oldRSs,然后会调用 syncDeploymentStatus 方法。 k8s.io/kubernetes/pkg/controller/deployment/sync.go:48 func (dc *DeploymentController) syncStatusOnly(d *apps.Deployment, rsList []*apps.ReplicaSet) error { newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return err } allRSs := append(oldRSs, newRS) return dc.syncDeploymentStatus(allRSs, newRS, d) } syncDeploymentStatus 首先通过 newRS 和 allRSs 计算 deployment 当前的 status,然后和 deployment 中的 status 进行比较,若二者有差异则更新 deployment 使用最新的 status,syncDeploymentStatus 在后面的多种操作中都会被用到。 k8s.io/kubernetes/pkg/controller/deployment/sync.go:469 func (dc *DeploymentController) syncDeploymentStatus(allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet, d *apps.Deployment) error { newStatus := calculateStatus(allRSs, newRS, d) if reflect.DeepEqual(d.Status, newStatus) { return nil } newDeployment := d newDeployment.Status = newStatus _, err := dc.client.AppsV1().Deployments(newDeployment.Namespace).UpdateStatus(newDeployment) return err } calculateStatus 如下所示,主要是通过 allRSs 以及 deployment 的状态计算出最新的 status。 k8s.io/kubernetes/pkg/controller/deployment/sync.go:483 func calculateStatus(allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet, deployment *apps.Deployment) apps.DeploymentStatus { availableReplicas := deploymentutil.GetAvailableReplicaCountForReplicaSets(allRSs) totalReplicas := deploymentutil.GetReplicaCountForReplicaSets(allRSs) unavailableReplicas := totalReplicas - availableReplicas if unavailableReplicas 以上就是 controller 中处理删除逻辑的主要流程,通过上述代码可知,当删除 deployment 对象时,仅仅是判断该对象中是否存在 metadata.DeletionTimestamp 字段,然后进行一次状态同步,并没有看到删除 deployment、rs、pod 对象的操作,其实删除对象并不是在此处进行而是在 kube-controller-manager 的垃圾回收器(garbagecollector controller)中完成的,对于 garbagecollector controller 会在后面的文章中进行说明,此外在删除对象时还需要指定一个删除选项(orphan、background 或者 foreground)来说明该对象如何删除。 暂停和恢复 暂停以及恢复两个操作都是通过更新 deployment spec.paused 字段实现的,下面直接看它的具体实现。 func (dc *DeploymentController) syncDeployment(key string) error { ...... // pause 操作 if d.Spec.Paused { return dc.sync(d, rsList) } if getRollbackTo(d) != nil { return dc.rollback(d, rsList) } // scale 操作 scalingEvent, err := dc.isScalingEvent(d, rsList) if err != nil { return err } if scalingEvent { return dc.sync(d, rsList) } ...... } 当触发暂停操作时,会调用 sync 方法进行操作,sync 方法的主要逻辑如下所示: 1、获取 newRS 和 oldRSs; 2、根据 newRS 和 oldRSs 判断是否需要 scale 操作; 3、若处于暂停状态且没有执行回滚操作,则根据 deployment 的 .spec.revisionHistoryLimit 中的值清理多余的 rs; 4、最后执行 syncDeploymentStatus 更新 status; func (dc *DeploymentController) sync(d *apps.Deployment, rsList []*apps.ReplicaSet) error { newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return err } if err := dc.scale(d, newRS, oldRSs); err != nil { return err } if d.Spec.Paused && getRollbackTo(d) == nil { if err := dc.cleanupDeployment(oldRSs, d); err != nil { return err } } allRSs := append(oldRSs, newRS) return dc.syncDeploymentStatus(allRSs, newRS, d) } 上文已经提到过 deployment controller 在一个 syncLoop 中各种操作是有优先级,而 pause > rollback > scale > rollout,通过文章开头的命令行参数也可以看出,暂停和恢复操作只有在 rollout 时才会生效,再结合源码分析,虽然暂停操作下不会执行到 scale 相关的操作,但是 pause 与 scale 都是调用 sync 方法完成的,且在 sync 方法中会首先检查 scale 操作是否完成,也就是说在 pause 操作后并不是立即暂停所有操作,例如,当执行滚动更新操作后立即执行暂停操作,此时滚动更新的第一个周期并不会立刻停止而是会等到滚动更新的第一个周期完成后才会处于暂停状态,在下文的滚动更新一节会有例子进行详细的分析,至于 scale 操作在下文也会进行详细分析。 syncDeploymentStatus 方法以及相关的代码在上文的删除操作中已经解释过了,此处不再进行分析。 回滚 kubernetes 中的每一个 Deployment 资源都包含有 revision 这个概念,并且其 .spec.revisionHistoryLimit 字段指定了需要保留的历史版本数,默认为10,每个版本都会对应一个 rs,若发现集群中有大量 0/0 rs 时请不要删除它,这些 rs 对应的都是 deployment 的历史版本,否则会导致无法回滚。当一个 deployment 的历史 rs 数超过指定数时,deployment controller 会自动清理。 当在客户端触发回滚操作时,controller 会调用 getRollbackTo 进行判断并调用 rollback 执行对应的回滚操作。 func (dc *DeploymentController) syncDeployment(key string) error { ...... if getRollbackTo(d) != nil { return dc.rollback(d, rsList) } ...... } getRollbackTo 通过判断 deployment 是否存在 rollback 对应的注解然后获取其值作为目标版本。 func getRollbackTo(d *apps.Deployment) *extensions.RollbackConfig { // annotations 为 \"deprecated.deployment.rollback.to\" revision := d.Annotations[apps.DeprecatedRollbackTo] if revision == \"\" { return nil } revision64, err := strconv.ParseInt(revision, 10, 64) if err != nil { return nil } return &extensions.RollbackConfig{ Revision: revision64, } } rollback 方法的主要逻辑如下: 1、获取 newRS 和 oldRSs; 2、调用 getRollbackTo 获取 rollback 的 revision; 3、判断 revision 以及对应的 rs 是否存在,若 revision 为 0,则表示回滚到上一个版本; 4、若存在对应的 rs,则调用 rollbackToTemplate 方法将 rs.Spec.Template 赋值给 d.Spec.Template,否则放弃回滚操作; k8s.io/kubernetes/pkg/controller/deployment/rollback.go:32 func (dc *DeploymentController) rollback(d *apps.Deployment, rsList []*apps.ReplicaSet) error { // 1、获取 newRS 和 oldRSs newRS, allOldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, true) if err != nil { return err } allRSs := append(allOldRSs, newRS) // 2、调用 getRollbackTo 获取 rollback 的 revision rollbackTo := getRollbackTo(d) // 3、判断 revision 以及对应的 rs 是否存在,若 revision 为 0,则表示回滚到最新的版本 if rollbackTo.Revision == 0 { if rollbackTo.Revision = deploymentutil.LastRevision(allRSs); rollbackTo.Revision == 0 { // 4、清除回滚标志放弃回滚操作 return dc.updateDeploymentAndClearRollbackTo(d) } } for _, rs := range allRSs { v, err := deploymentutil.Revision(rs) if err != nil { ...... } if v == rollbackTo.Revision { // 5、调用 rollbackToTemplate 进行回滚操作 performedRollback, err := dc.rollbackToTemplate(d, rs) if performedRollback && err == nil { ...... } return err } } return dc.updateDeploymentAndClearRollbackTo(d) } rollbackToTemplate 会判断 deployment.Spec.Template 和 rs.Spec.Template 是否相等,若相等则无需回滚,否则使用 rs.Spec.Template 替换 deployment.Spec.Template,然后更新 deployment 的 spec 并清除回滚标志。 k8s.io/kubernetes/pkg/controller/deployment/rollback.go:75 func (dc *DeploymentController) rollbackToTemplate(d *apps.Deployment, rs *apps.ReplicaSet) (bool, error) { performedRollback := false // 1、比较 d.Spec.Template 和 rs.Spec.Template 是否相等 if !deploymentutil.EqualIgnoreHash(&d.Spec.Template, &rs.Spec.Template) { // 2、替换 d.Spec.Template deploymentutil.SetFromReplicaSetTemplate(d, rs.Spec.Template) // 3、设置 annotation deploymentutil.SetDeploymentAnnotationsTo(d, rs) performedRollback = true } else { dc.emitRollbackWarningEvent(d, deploymentutil.RollbackTemplateUnchanged, eventMsg) } // 4、更新 deployment 并清除回滚标志 return performedRollback, dc.updateDeploymentAndClearRollbackTo(d) } 回滚操作其实就是通过 revision 找到对应的 rs,然后使用 rs.Spec.Template 替换 deployment.Spec.Template 最后驱动 replicaSet 和 pod 达到期望状态即完成了回滚操作,在最新版中,这种使用注解方式指定回滚版本的方法即将被废弃。 扩缩容 当执行 scale 操作时,首先会通过 isScalingEvent 方法判断是否为扩缩容操作,然后通过 dc.sync 方法来执行实际的扩缩容动作。 func (dc *DeploymentController) syncDeployment(key string) error { ...... // scale 操作 scalingEvent, err := dc.isScalingEvent(d, rsList) if err != nil { return err } if scalingEvent { return dc.sync(d, rsList) } ...... } isScalingEvent 的主要逻辑如下所示: 1、获取所有的 rs; 2、过滤出 activeRS,rs.Spec.Replicas > 0 的为 activeRS; 3、判断 rs 的 desired 值是否等于 deployment.Spec.Replicas,若不等于则需要为 rs 进行 scale 操作; k8s.io/kubernetes/pkg/controller/deployment/sync.go:526 func (dc *DeploymentController) isScalingEvent(......) (bool, error) { // 1、获取所有 rs newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return false, err } allRSs := append(oldRSs, newRS) // 2、过滤出 activeRS 并进行比较 for _, rs := range controller.FilterActiveReplicaSets(allRSs) { // 3、获取 rs annotation 中 deployment.kubernetes.io/desired-replicas 的值 desired, ok := deploymentutil.GetDesiredReplicasAnnotation(rs) if !ok { continue } // 4、判断是否需要 scale 操作 if desired != *(d.Spec.Replicas) { return true, nil } } return false, nil } 在通过 isScalingEvent 判断为 scale 操作时会调用 sync 方法执行,主要逻辑如下: 1、获取 newRS 和 oldRSs; 2、调用 scale 方法进行扩缩容操作; 3、同步 deployment 的状态; func (dc *DeploymentController) sync(d *apps.Deployment, rsList []*apps.ReplicaSet) error { newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return err } if err := dc.scale(d, newRS, oldRSs); err != nil { return err } ...... allRSs := append(oldRSs, newRS) return dc.syncDeploymentStatus(allRSs, newRS, d) } sync 方法中会调用 scale 方法执行扩容操作,其主要逻辑为: 1、通过 FindActiveOrLatest 获取 activeRS 或者最新的 rs,此时若只有一个 rs 说明本次操作仅为 scale 操作,则调用 scaleReplicaSetAndRecordEvent 对 rs 进行 scale 操作,否则此时存在多个 activeRS; 2、判断 newRS 是否已达到期望副本数,若达到则将所有的 oldRS 缩容到 0; 3、若 newRS 还未达到期望副本数,且存在多个 activeRS,说明此时的操作有可能是升级与扩缩容操作同时进行,若 deployment 的更新操作为 RollingUpdate 那么 scale 操作也需要按比例进行: 通过 FilterActiveReplicaSets 获取所有活跃的 ReplicaSet 对象; 调用 GetReplicaCountForReplicaSets 计算当前 Deployment 对应 ReplicaSet 持有的全部 Pod 副本个数; 计算 Deployment 允许创建的最大 Pod 数量; 判断是扩容还是缩容并对 allRSs 按时间戳进行正向或者反向排序; 计算每个 rs 需要增加或者删除的副本数; 更新 rs 对象; 4、若为 recreat 则需要等待更新完成后再进行 scale 操作; k8s.io/kubernetes/pkg/controller/deployment/sync.go:294 func (dc *DeploymentController) scale(......) error { // 1、在滚动更新过程中 第一个 rs 的 replicas 数量= maxSuger + dep.spec.Replicas , // 更新完成后 pod 数量会多出 maxSurge 个,此处若检测到则应缩减回去 if activeOrLatest := deploymentutil.FindActiveOrLatest(newRS, oldRSs); activeOrLatest != nil { if *(activeOrLatest.Spec.Replicas) == *(deployment.Spec.Replicas) { return nil } // 2、只更新 rs annotation 以及为 deployment 设置 events _, _, err := dc.scaleReplicaSetAndRecordEvent(activeOrLatest, *(deployment.Spec.Replicas), deployment) return err } // 3、当调用 IsSaturated 方法发现当前的 Deployment 对应的副本数量已经达到期望状态时就 // 将所有历史版本 rs 持有的副本缩容为 0 if deploymentutil.IsSaturated(deployment, newRS) { for _, old := range controller.FilterActiveReplicaSets(oldRSs) { if _, _, err := dc.scaleReplicaSetAndRecordEvent(old, 0, deployment); err != nil { return err } } return nil } // 4、此时说明 当前的 rs 副本并没有达到期望状态并且存在多个活跃的 rs 对象, // 若 deployment 的更新策略为滚动更新,需要按照比例分别对各个活跃的 rs 进行扩容或者缩容 if deploymentutil.IsRollingUpdate(deployment) { allRSs := controller.FilterActiveReplicaSets(append(oldRSs, newRS)) allRSsReplicas := deploymentutil.GetReplicaCountForReplicaSets(allRSs) allowedSize := int32(0) // 5、计算最大可以创建出的 pod 数 if *(deployment.Spec.Replicas) > 0 { allowedSize = *(deployment.Spec.Replicas) + deploymentutil.MaxSurge(*deployment) } // 6、计算需要扩容的 pod 数 deploymentReplicasToAdd := allowedSize - allRSsReplicas // 7、如果 deploymentReplicasToAdd > 0,ReplicaSet 将按照从新到旧的顺序依次进行扩容; // 如果 deploymentReplicasToAdd 0,则需要先扩容 newRS,但当在先扩容然后立刻缩容时,若 0: sort.Sort(controller.ReplicaSetsBySizeNewer(allRSs)) scalingOperation = \"up\" case deploymentReplicasToAdd 上述方法中有一个重要的操作就是在第 9 步调用 GetProportion 方法估算出 rs 需要扩容或者缩容的副本数,该方法中计算副本数的逻辑如下所示: k8s.io/kubernetes/pkg/controller/deployment/util/deployment_util.go:466 func GetProportion(rs *apps.ReplicaSet, d apps.Deployment, deploymentReplicasToAdd, deploymentReplicasAdded int32) int32 { if rs == nil || *(rs.Spec.Replicas) == 0 || deploymentReplicasToAdd == 0 || deploymentReplicasToAdd == deploymentReplicasAdded { return int32(0) } // 调用 getReplicaSetFraction 方法 rsFraction := getReplicaSetFraction(*rs, d) allowed := deploymentReplicasToAdd - deploymentReplicasAdded if deploymentReplicasToAdd > 0 { return integer.Int32Min(rsFraction, allowed) } return integer.Int32Max(rsFraction, allowed) } func getReplicaSetFraction(rs apps.ReplicaSet, d apps.Deployment) int32 { if *(d.Spec.Replicas) == int32(0) { return -*(rs.Spec.Replicas) } deploymentReplicas := *(d.Spec.Replicas) + MaxSurge(d) annotatedReplicas, ok := getMaxReplicasAnnotation(&rs) if !ok { annotatedReplicas = d.Status.Replicas } // 计算 newRSSize 的公式 newRSsize := (float64(*(rs.Spec.Replicas) * deploymentReplicas)) / float64(annotatedReplicas) // 返回最终计算出的结果 return integer.RoundToInt32(newRSsize) - *(rs.Spec.Replicas) } 滚动更新 deployment 的更新方式有两种,其中滚动更新是最常用的,下面就看看其具体的实现。 func (dc *DeploymentController) syncDeployment(key string) error { ...... switch d.Spec.Strategy.Type { case apps.RecreateDeploymentStrategyType: return dc.rolloutRecreate(d, rsList, podMap) case apps.RollingUpdateDeploymentStrategyType: // 调用 rolloutRolling 执行滚动更新 return dc.rolloutRolling(d, rsList) } ...... } 通过判断 d.Spec.Strategy.Type ,当更新操作为 rolloutRolling 时,会调用 rolloutRolling 方法进行操作,具体的逻辑如下所示: 1、调用 getAllReplicaSetsAndSyncRevision 获取所有的 rs,若没有 newRS 则创建; 2、调用 reconcileNewReplicaSet 判断是否需要对 newRS 进行 scaleUp 操作; 3、如果需要 scaleUp,更新 Deployment 的 status,添加相关的 condition,直接返回; 4、调用 reconcileOldReplicaSets 判断是否需要为 oldRS 进行 scaleDown 操作; 5、如果两者都不是则滚动升级很可能已经完成,此时需要检查 deployment status 是否已经达到期望状态,并且根据 deployment.Spec.RevisionHistoryLimit 的值清理 oldRSs; func (dc *DeploymentController) rolloutRolling(......) error { // 1、获取所有的 rs,若没有 newRS 则创建 newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, true) if err != nil { return err } allRSs := append(oldRSs, newRS) // 2、执行 scale up 操作 scaledUp, err := dc.reconcileNewReplicaSet(allRSs, newRS, d) if err != nil { return err } if scaledUp { return dc.syncRolloutStatus(allRSs, newRS, d) } // 3、执行 scale down 操作 scaledDown, err := dc.reconcileOldReplicaSets(allRSs, controller.FilterActiveReplicaSets(oldRSs), newRS, d) if err != nil { return err } if scaledDown { return dc.syncRolloutStatus(allRSs, newRS, d) } // 4、清理过期的 rs if deploymentutil.DeploymentComplete(d, &d.Status) { if err := dc.cleanupDeployment(oldRSs, d); err != nil { return err } } // 5、同步 deployment status return dc.syncRolloutStatus(allRSs, newRS, d) } reconcileNewReplicaSet 主要逻辑如下: 1、判断 newRS.Spec.Replicas 和 deployment.Spec.Replicas 是否相等,如果相等则直接返回,说明已经达到期望状态; 2、若 newRS.Spec.Replicas > deployment.Spec.Replicas ,则说明 newRS 副本数已经超过期望值,调用 dc.scaleReplicaSetAndRecordEvent 进行 scale down; 3、此时 newRS.Spec.Replicas deployment.Spec.Replicas ,调用 NewRSNewReplicas 为 newRS 计算所需要的副本数,计算原则遵守 maxSurge 和 maxUnavailable 的约束; 4、调用 scaleReplicaSetAndRecordEvent 更新 newRS 对象,设置 rs.Spec.Replicas、rs.Annotations[DesiredReplicasAnnotation] 以及 rs.Annotations[MaxReplicasAnnotation] ; k8s.io/kubernetes/pkg/controller/deployment/rolling.go:69 func (dc *DeploymentController) reconcileNewReplicaSet(......) (bool, error) { // 1、判断副本数是否已达到了期望值 if *(newRS.Spec.Replicas) == *(deployment.Spec.Replicas) { return false, nil } // 2、判断是否需要 scale down 操作 if *(newRS.Spec.Replicas) > *(deployment.Spec.Replicas) { scaled, _, err := dc.scaleReplicaSetAndRecordEvent(newRS, *(deployment.Spec.Replicas), deployment) return scaled, err } // 3、计算 newRS 所需要的副本数 newReplicasCount, err := deploymentutil.NewRSNewReplicas(deployment, allRSs, newRS) if err != nil { return false, err } // 4、如果需要 scale ,则更新 rs 的 annotation 以及 rs.Spec.Replicas scaled, _, err := dc.scaleReplicaSetAndRecordEvent(newRS, newReplicasCount, deployment) return scaled, err } NewRSNewReplicas 是为 newRS 计算所需要的副本数,该方法主要逻辑为: 1、判断更新策略; 2、计算 maxSurge 值; 3、通过 allRSs 计算 currentPodCount 的值; 4、最后计算 scaleUpCount 值; k8s.io/kubernetes/pkg/controller/deployment/util/deployment_util.go:814 func NewRSNewReplicas(......) (int32, error) { switch deployment.Spec.Strategy.Type { case apps.RollingUpdateDeploymentStrategyType: // 1、计算 maxSurge 值 maxSurge, err := intstrutil.GetValueFromIntOrPercent(deployment.Spec.Strategy.RollingUpdate.MaxSurge, int(*(deployment.Spec.Replicas)), true) if err != nil { return 0, err } // 2、累加 rs.Spec.Replicas 获取 currentPodCount currentPodCount := GetReplicaCountForReplicaSets(allRSs) maxTotalPods := *(deployment.Spec.Replicas) + int32(maxSurge) if currentPodCount >= maxTotalPods { return *(newRS.Spec.Replicas), nil } // 3、计算 scaleUpCount scaleUpCount := maxTotalPods - currentPodCount scaleUpCount = int32(integer.IntMin(int(scaleUpCount), int(*(deployment.Spec.Replicas)-*(newRS.Spec.Replicas)))) return *(newRS.Spec.Replicas) + scaleUpCount, nil case apps.RecreateDeploymentStrategyType: return *(deployment.Spec.Replicas), nil default: return 0, fmt.Errorf(\"deployment type %v isn't supported\", deployment.Spec.Strategy.Type) } } reconcileOldReplicaSets 的主要逻辑如下: 1、通过 oldRSs 和 allRSs 获取 oldPodsCount 和 allPodsCount; 2、计算 deployment 的 maxUnavailable、minAvailable、newRSUnavailablePodCount、maxScaledDown 值,当 deployment 的 maxSurge 和 maxUnavailable 值为百分数时,计算 maxSurge 向上取整而 maxUnavailable 则向下取整; 3、清理异常的 rs; 4、计算 oldRS 的 scaleDownCount; func (dc *DeploymentController) reconcileOldReplicaSets(......) (bool, error) { // 1、计算 oldPodsCount oldPodsCount := deploymentutil.GetReplicaCountForReplicaSets(oldRSs) if oldPodsCount == 0 { return false, nil } // 2、计算 allPodsCount allPodsCount := deploymentutil.GetReplicaCountForReplicaSets(allRSs) // 3、计算 maxScaledDown maxUnavailable := deploymentutil.MaxUnavailable(*deployment) minAvailable := *(deployment.Spec.Replicas) - maxUnavailable newRSUnavailablePodCount := *(newRS.Spec.Replicas) - newRS.Status.AvailableReplicas maxScaledDown := allPodsCount - minAvailable - newRSUnavailablePodCount if maxScaledDown 0, nil } 通过上面的代码可以看出,滚动更新过程中主要是通过调用reconcileNewReplicaSet对 newRS 不断扩容,调用 reconcileOldReplicaSets 对 oldRS 不断缩容,最终达到期望状态,并且在整个升级过程中,都严格遵守 maxSurge 和 maxUnavailable 的约束。 不论是在 scale up 或者 scale down 中都是调用 scaleReplicaSetAndRecordEvent 执行,而 scaleReplicaSetAndRecordEvent 又会调用 scaleReplicaSet 来执行,两个操作都是更新 rs 的 annotations 以及 rs.Spec.Replicas。 scale down or --> dc.scaleReplicaSetAndRecordEvent() --> dc.scaleReplicaSet() scale up 滚动更新示例 上面的代码看起来非常的枯燥,只看源码其实并不能完全理解整个滚动升级的流程,此处举个例子说明一下: 创建一个 nginx-deployment 有10 个副本,等 10 个 pod 都启动完成后如下所示: $ kubectl create -f nginx-dep.yaml $ kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deployment-68b649bd8b 10 10 10 72m 然后更新 nginx-deployment 的镜像,默认使用滚动更新的方式: $ kubectl set image deploy/nginx-deployment nginx-deployment=nginx:1.9.3 此时通过源码可知会计算该 deployment 的 maxSurge、maxUnavailable 和 maxAvailable 的值,分别为 3、2 和 13,计算方法如下所示: // 向上取整为 3 maxSurge = replicas * deployment.spec.strategy.rollingUpdate.maxSurge(25%)= 2.5 // 向下取整为 2 maxUnavailable = replicas * deployment.spec.strategy.rollingUpdate.maxUnavailable(25%)= 2.5 maxAvailable = replicas(10) + MaxSurge(3) = 13 如上面代码所说,更新时首先创建 newRS,然后为其设定 replicas,计算 newRS replicas 值的方法在NewRSNewReplicas 中,此时计算出 replicas 结果为 3,然后更新 deployment 的 annotation,创建 events,本次 syncLoop 完成。等到下一个 syncLoop 时,所有 rs 的 replicas 已经达到最大值 10 + 3 = 13,此时需要 scale down oldRSs 了,scale down 的数量是通过以下公式得到的: // 13 = 10 + 3 allPodsCount := deploymentutil.GetReplicaCountForReplicaSets(allRSs) // 8 = 10 - 2 minAvailable := *(deployment.Spec.Replicas) - maxUnavailable // ??? newRSUnavailablePodCount := *(newRS.Spec.Replicas) - newRS.Status.AvailableReplicas // 13 - 8 - ??? maxScaledDown := allPodsCount - minAvailable - newRSUnavailablePodCount allPodsCount 是 allRSs 的 replicas 之和此时为 13,minAvailable 为 8 ,newRSUnavailablePodCount 此时不确定,但是值在 [0,3] 中,此时假设 newRS 的三个 pod 还处于 containerCreating 状态,则newRSUnavailablePodCount 为 3,根据以上公式计算所知 maxScaledDown 为 2,则 oldRS 需要 scale down 2 个 pod,其 replicas 需要改为 8,此时该 syncLoop 完成。下一个 syncLoop 时在 scaleUp 处计算得知 scaleUpCount = maxTotalPods - currentPodCount,13-3-8=2, 此时 newRS 需要更新 replicase 增加 2。以此轮询直到 newRS replicas 扩容到 10,oldRSs replicas 缩容至 0。 对于上面的示例,可以使用 kubectl get rs -w 进行观察,以下为输出: $ kubectl get rs -w NAME DESIRED CURRENT READY AGE nginx-deployment-68b649bd8b 10 0 0 0s nginx-deployment-68b649bd8b 10 10 0 0s nginx-deployment-68b649bd8b 10 10 10 13s nginx-deployment-689bff574f 3 0 0 0s nginx-deployment-68b649bd8b 8 10 10 14s nginx-deployment-689bff574f 3 0 0 0s nginx-deployment-689bff574f 3 3 3 1s nginx-deployment-689bff574f 5 3 0 0s nginx-deployment-68b649bd8b 8 8 8 14s nginx-deployment-689bff574f 5 3 0 0s nginx-deployment-689bff574f 5 5 0 0s nginx-deployment-689bff574f 5 5 5 6s ...... 重新创建 deployment 的另一种更新策略recreate 就比较简单粗暴了,当更新策略为 Recreate 时,deployment 先将所有旧的 rs 缩容到 0,并等待所有 pod 都删除后,再创建新的 rs。 func (dc *DeploymentController) syncDeployment(key string) error { ...... switch d.Spec.Strategy.Type { case apps.RecreateDeploymentStrategyType: return dc.rolloutRecreate(d, rsList, podMap) case apps.RollingUpdateDeploymentStrategyType: return dc.rolloutRolling(d, rsList) } ...... } rolloutRecreate 方法主要逻辑为: 1、获取 newRS 和 oldRSs; 2、缩容 oldRS replicas 至 0; 3、创建 newRS; 4、扩容 newRS; 5、同步 deployment 状态; func (dc *DeploymentController) rolloutRecreate(......) error { // 1、获取所有 rs newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return err } allRSs := append(oldRSs, newRS) activeOldRSs := controller.FilterActiveReplicaSets(oldRSs) // 2、缩容 oldRS scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d) if err != nil { return err } if scaledDown { return dc.syncRolloutStatus(allRSs, newRS, d) } if oldPodsRunning(newRS, oldRSs, podMap) { return dc.syncRolloutStatus(allRSs, newRS, d) } // 3、创建 newRS if newRS == nil { newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList, true) if err != nil { return err } allRSs = append(oldRSs, newRS) } // 4、扩容 newRS if _, err := dc.scaleUpNewReplicaSetForRecreate(newRS, d); err != nil { return err } // 5、清理过期的 RS if util.DeploymentComplete(d, &d.Status) { if err := dc.cleanupDeployment(oldRSs, d); err != nil { return err } } // 6、同步 deployment 状态 return dc.syncRolloutStatus(allRSs, newRS, d) } 判断 deployment 是否存在 newRS 是在 deploymentutil.FindNewReplicaSet 方法中进行判断的,对比 rs.Spec.Template 和 deployment.Spec.Template 中字段的 hash 值是否相等以此进行确定,在上面的几个操作中也多次用到了该方法,此处说明一下。 dc.getAllReplicaSetsAndSyncRevision() --> dc.getNewReplicaSet() --> deploymentutil.FindNewReplicaSet() --> EqualIgnoreHash() EqualIgnoreHash 方法如下所示: k8s.io/kubernetes/pkg/controller/deployment/util/deployment_util.go:633 func EqualIgnoreHash(template1, template2 *v1.PodTemplateSpec) bool { t1Copy := template1.DeepCopy() t2Copy := template2.DeepCopy() // Remove hash labels from template.Labels before comparing delete(t1Copy.Labels, apps.DefaultDeploymentUniqueLabelKey) delete(t2Copy.Labels, apps.DefaultDeploymentUniqueLabelKey) return apiequality.Semantic.DeepEqual(t1Copy, t2Copy) } 以上就是对 deployment recreate 更新策略源码的分析,需要注意的是,该策略会导致服务一段时间不可用,当 oldRS 缩容为 0,newRS 才开始创建,此时无可用的 pod,所以在生产环境中请慎用该更新策略。 总结 本文主要介绍了 deployment 的基本功能以及从源码角度分析其实现,deployment 主要有更新、回滚、扩缩容、暂停与恢复几个主要的功能。从源码中可以看到 deployment 在升级过程中一直会修改 rs 的 replicas 以及 annotation 最终达到最终期望的状态,但是整个过程中并没有体现出 pod 的创建与删除,从开头三者的关系图中可知是 rs 控制 pod 的变化,在下篇文章中会继续介绍 rs 是如何控制 pod 的变化。 参考: https://my.oschina.net/u/3797264/blog/2966086 https://draveness.me/kubernetes-deployment Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/replicaset_controller.html":{"url":"kubernetes/replicaset_controller.html","title":"replicaset controller 源码分析","keywords":"","body":"在前面的文章中已经介绍了 deployment controller 的设计与实现,deployment 控制的是 replicaset,而 replicaset 控制 pod 的创建与删除,deployment 通过控制 replicaset 实现了滚动更新、回滚等操作。而 replicaset 会直接控制 pod 的创建与删除,本文会继续从源码层面分析 replicaset 的设计与实现。 在分析源码前先考虑一下 replicaset 的使用场景,在平时的操作中其实我们并不会直接操作 replicaset,replicaset 也仅有几个简单的操作,创建、删除、更新等,但其地位是非常重要的,replicaset 的主要功能就是通过 add/del pod 来达到期望的状态。 ReplicaSetController 源码分析 kubernetes 版本: v1.16 启动流程 首先来看 replicaSetController 对象初始化以及启动的代码,在 startReplicaSetController 中有两个比较重要的变量: BurstReplicas:用来控制在一个 syncLoop 过程中 rs 最多能创建的 pod 数量,设置上限值是为了避免单个 rs 影响整个系统,默认值为 500; ConcurrentRSSyncs:指的是需要启动多少个 goroutine 处理 informer 队列中的对象,默认值为 5; k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:69 func startReplicaSetController(ctx ControllerContext) (http.Handler, bool, error) { if !ctx.AvailableResources[schema.GroupVersionResource{Group: \"apps\", Version: \"v1\", Resource: \"replicasets\"}] { return nil, false, nil } go replicaset.NewReplicaSetController( ctx.InformerFactory.Apps().V1().ReplicaSets(), ctx.InformerFactory.Core().V1().Pods(), ctx.ClientBuilder.ClientOrDie(\"replicaset-controller\"), replicaset.BurstReplicas, ).Run(int(ctx.ComponentConfig.ReplicaSetController.ConcurrentRSSyncs), ctx.Stop) return nil, true, nil } 下面是 replicaSetController 初始化的具体步骤,可以看到其会监听 pod 以及 rs 两个对象的事件。 k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:109 func NewReplicaSetController(......) *ReplicaSetController { ...... // 1、此处调用 NewBaseController return NewBaseController(rsInformer, podInformer, kubeClient, burstReplicas, apps.SchemeGroupVersion.WithKind(\"ReplicaSet\"), \"replicaset_controller\", \"replicaset\", controller.RealPodControl{ KubeClient: kubeClient, Recorder: eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: \"replicaset-controller\"}), }, ) } func NewBaseController(......) *ReplicaSetController { ...... // 2、ReplicaSetController 初始化 rsc := &ReplicaSetController{ GroupVersionKind: gvk, kubeClient: kubeClient, podControl: podControl, burstReplicas: burstReplicas, // 3、expectations 的初始化 expectations: controller.NewUIDTrackingControllerExpectations(controller.NewControllerExpectations()), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), queueName), } // 4、rsInformer 中注册的 EventHandler rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: rsc.enqueueReplicaSet, UpdateFunc: rsc.updateRS, DeleteFunc: rsc.enqueueReplicaSet, }) ...... // 5、podInformer 中注册的 EventHandler podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: rsc.addPod, UpdateFunc: rsc.updatePod, DeleteFunc: rsc.deletePod, }) ...... return rsc } replicaSetController 初始化完成后会调用 Run 方法启动 5 个 goroutine 处理 informer 队列中的事件并进行 sync 操作,kube-controller-manager 中每个 controller 的启动操作都是如下所示流程。 k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:177 func (rsc *ReplicaSetController) Run(workers int, stopCh EventHandler 初始化 replicaSetController 时,其中有一个 expectations 字段,这是 rs 中一个比较特殊的机制,为了说清楚 expectations,先来看一下 controller 中所注册的 eventHandler,replicaSetController 会 watch pod 和 replicaSet 两个对象,eventHandler 中注册了对这两种对象的 add、update、delete 三个操作。 addPod 1、判断 pod 是否处于删除状态; 2、获取该 pod 关联的 rs 以及 rsKey,入队 rs 并更新 rsKey 的 expectations; 3、若 pod 对象没体现出关联的 rs 则为孤儿 pod,遍历 rsList 查找匹配的 rs,若该 rs.Namespace == pod.Namespace 并且 rs.Spec.Selector 匹配 pod.Labels,则说明该 pod 应该与此 rs 关联,将匹配的 rs 入队; k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:255 func (rsc *ReplicaSetController) addPod(obj interface{}) { pod := obj.(*v1.Pod) if pod.DeletionTimestamp != nil { rsc.deletePod(pod) return } // 1、获取 pod 所关联的 rs if controllerRef := metav1.GetControllerOf(pod); controllerRef != nil { rs := rsc.resolveControllerRef(pod.Namespace, controllerRef) if rs == nil { return } rsKey, err := controller.KeyFunc(rs) if err != nil { return } // 2、更新 expectations,rsKey 的 add - 1 rsc.expectations.CreationObserved(rsKey) rsc.enqueueReplicaSet(rs) return } rss := rsc.getPodReplicaSets(pod) if len(rss) == 0 { return } for _, rs := range rss { rsc.enqueueReplicaSet(rs) } } updatePod 1、如果 pod label 改变或者处于删除状态,则直接删除; 2、如果 pod 的 OwnerReference 发生改变,此时 oldRS 需要创建 pod,将 oldRS 入队; 3、获取 pod 关联的 rs,入队 rs,若 pod 当前处于 ready 并非 available 状态,则会再次将该 rs 加入到延迟队列中,因为 pod 从 ready 到 available 状态需要触发一次 status 的更新; 4、否则为孤儿 pod,遍历 rsList 查找匹配的 rs,若找到则将 rs 入队; k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:298 func (rsc *ReplicaSetController) updatePod(old, cur interface{}) { curPod := cur.(*v1.Pod) oldPod := old.(*v1.Pod) if curPod.ResourceVersion == oldPod.ResourceVersion { return } // 1、如果 pod label 改变或者处于删除状态,则直接删除 labelChanged := !reflect.DeepEqual(curPod.Labels, oldPod.Labels) if curPod.DeletionTimestamp != nil { rsc.deletePod(curPod) if labelChanged { rsc.deletePod(oldPod) } return } // 2、如果 pod 的 OwnerReference 发生改变,将 oldRS 入队 curControllerRef := metav1.GetControllerOf(curPod) oldControllerRef := metav1.GetControllerOf(oldPod) controllerRefChanged := !reflect.DeepEqual(curControllerRef, oldControllerRef) if controllerRefChanged && oldControllerRef != nil { if rs := rsc.resolveControllerRef(oldPod.Namespace, oldControllerRef); rs != nil { rsc.enqueueReplicaSet(rs) } } // 3、获取 pod 关联的 rs,入队 rs if curControllerRef != nil { rs := rsc.resolveControllerRef(curPod.Namespace, curControllerRef) if rs == nil { return } rsc.enqueueReplicaSet(rs) if !podutil.IsPodReady(oldPod) && podutil.IsPodReady(curPod) && rs.Spec.MinReadySeconds > 0 { rsc.enqueueReplicaSetAfter(rs, (time.Duration(rs.Spec.MinReadySeconds)*time.Second)+time.Second) } return } // 4、查找匹配的 rs if labelChanged || controllerRefChanged { rss := rsc.getPodReplicaSets(curPod) if len(rss) == 0 { return } for _, rs := range rss { rsc.enqueueReplicaSet(rs) } } } deletePod 1、确认该对象是否为 pod; 2、判断是否为孤儿 pod; 3、获取其对应的 rs 以及 rsKey; 4、更新 expectations 中 rsKey 的 del 值; 5、将 rs 入队; k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:372 func (rsc *ReplicaSetController) deletePod(obj interface{}) { pod, ok := obj.(*v1.Pod) if !ok { ...... } controllerRef := metav1.GetControllerOf(pod) if controllerRef == nil { return } rs := rsc.resolveControllerRef(pod.Namespace, controllerRef) if rs == nil { return } rsKey, err := controller.KeyFunc(rs) if err != nil { return } // 更新 expectations,该 rsKey 的 del - 1 rsc.expectations.DeletionObserved(rsKey, controller.PodKey(pod)) rsc.enqueueReplicaSet(rs) } AddRS 和 DeleteRS 以上两个操作仅仅是将对应的 rs 入队。 UpdateRS 其实 updateRS 也仅仅是将对应的 rs 进行入队,不过多了一个打印日志的操作,如下所示: k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:232 func (rsc *ReplicaSetController) updateRS(old, cur interface{}) { oldRS := old.(*apps.ReplicaSet) curRS := cur.(*apps.ReplicaSet) if *(oldRS.Spec.Replicas) != *(curRS.Spec.Replicas) { klog.V(4).Infof(\"%v %v updated. Desired pod count change: %d->%d\", rsc.Kind, curRS.Name, *(oldRS.Spec.Replicas), *(curRS.Spec.Replicas)) } rsc.enqueueReplicaSet(cur) } 至于 expectations 机制会在下文进行分析。 syncReplicaSet syncReplicaSet 是 controller 的核心方法,它会驱动 controller 所控制的对象达到期望状态,主要逻辑如下所示: 1、根据 ns/name 获取 rs 对象; 2、调用 expectations.SatisfiedExpectations 判断是否需要执行真正的 sync 操作; 3、获取所有 pod list; 4、根据 pod label 进行过滤获取与该 rs 关联的 pod 列表,对于其中的孤儿 pod 若与该 rs label 匹配则进行关联,若已关联的 pod 与 rs label 不匹配则解除关联关系; 5、调用 manageReplicas 进行同步 pod 操作,add/del pod; 6、计算 rs 当前的 status 并进行更新; 7、若 rs 设置了 MinReadySeconds 字段则将该 rs 加入到延迟队列中; k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:562 func (rsc *ReplicaSetController) syncReplicaSet(key string) error { ...... namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } // 1、根据 ns/name 从 informer cache 中获取 rs 对象, // 若 rs 已经被删除则直接删除 expectations 中的对象 rs, err := rsc.rsLister.ReplicaSets(namespace).Get(name) if errors.IsNotFound(err) { rsc.expectations.DeleteExpectations(key) return nil } ...... // 2、判断该 rs 是否需要执行 sync 操作 rsNeedsSync := rsc.expectations.SatisfiedExpectations(key) selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) if err != nil { ...... } // 3、获取所有 pod list allPods, err := rsc.podLister.Pods(rs.Namespace).List(labels.Everything()) ...... // 4、过滤掉异常 pod,处于删除状态或者 failed 状态的 pod 都为非 active 状态 filteredPods := controller.FilterActivePods(allPods) // 5、检查所有 pod,根据 pod 并进行 adopt 与 release 操作,最后获取与该 rs 关联的 pod list filteredPods, err = rsc.claimPods(rs, selector, filteredPods) ...... // 6、若需要 sync 则执行 manageReplicas 创建/删除 pod var manageReplicasErr error if rsNeedsSync && rs.DeletionTimestamp == nil { manageReplicasErr = rsc.manageReplicas(filteredPods, rs) } rs = rs.DeepCopy() // 7、计算 rs 当前的 status newStatus := calculateStatus(rs, filteredPods, manageReplicasErr) // 8、更新 rs status updatedRS, err := updateReplicaSetStatus(rsc.kubeClient.AppsV1().ReplicaSets(rs.Namespace), rs, newStatus) // 9、判断是否需要将 rs 加入到延迟队列中 if manageReplicasErr == nil && updatedRS.Spec.MinReadySeconds > 0 && updatedRS.Status.ReadyReplicas == *(updatedRS.Spec.Replicas) && updatedRS.Status.AvailableReplicas != *(updatedRS.Spec.Replicas) { rsc.enqueueReplicaSetAfter(updatedRS, time.Duration(updatedRS.Spec.MinReadySeconds)*time.Second) } return manageReplicasErr } 在 syncReplicaSet 方法中有几个重要的操作分别为:rsc.expectations.SatisfiedExpectations、rsc.manageReplicas、calculateStatus,下面一一进行分析。 SatisfiedExpectations 该方法主要判断 rs 是否需要执行真正的同步操作,若需要 add/del pod 或者 expectations 已过期则需要进行同步操作。 k8s.io/kubernetes/pkg/controller/controller_utils.go:181 func (r *ControllerExpectations) SatisfiedExpectations(controllerKey string) bool { // 1、若该 key 存在时,判断是否满足条件或者是否超过同步周期 if exp, exists, err := r.GetExpectations(controllerKey); exists { if exp.Fulfilled() { return true } else if exp.isExpired() { return true } else { return false } } else if err != nil { ...... } else { // 2、该 rs 可能为新创建的,需要进行 sync ...... } return true } // 3、若 add ExpectationsTimeout } manageReplicas manageReplicas 是最核心的方法,它会计算 replicaSet 需要创建或者删除多少个 pod 并调用 apiserver 的接口进行操作,在此阶段仅仅是调用 apiserver 的接口进行创建,并不保证 pod 成功运行,如果在某一轮,未能成功创建的所有 Pod 对象,则不再创建剩余的 pod。一个周期内最多只能创建或删除 500 个 pod,若超过上限值未创建完成的 pod 数会在下一个 syncLoop 继续进行处理。 该方法主要逻辑如下所示: 1、计算已存在 pod 数与期望数的差异; 2、如果 diff 3、如果 diff > 0 说明可能是一次缩容操作需要删除多余的 pod,如果需要删除全部的 pod 则直接进行删除,否则会通过 getPodsToDelete 方法筛选出需要删除的 pod,具体的筛选策略在下文会将到,然后并发删除这些 pod,对于删除失败操作也会记录在 expectations 中; 在 slowStartBatch 中会调用 rsc.podControl.CreatePodsWithControllerRef 方法创建 pod,若创建 pod 失败会判断是否为创建超时错误,或者可能是超时后失败,但此时认为超时并不影响后续的批量创建动作,大家知道,创建 pod 操作提交到 apiserver 后会经过认证、鉴权、以及动态访问控制三个步骤,此过程有可能会超时,即使真的创建失败了,等到 expectations 过期后在下一个 syncLoop 时会重新创建。 k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:459 func (rsc *ReplicaSetController) manageReplicas(......) error { // 1、计算已存在 pod 数与期望数的差异 diff := len(filteredPods) - int(*(rs.Spec.Replicas)) rsKey, err := controller.KeyFunc(rs) if err != nil { ...... } 2、如果 rsc.burstReplicas { diff = rsc.burstReplicas } 4、在 expectations 中进行记录,若该 key 已经存在会进行覆盖 rsc.expectations.ExpectCreations(rsKey, diff) 5、调用 slowStartBatch 创建所需要的 pod successfulCreations, err := slowStartBatch(diff, controller.SlowStartInitialBatchSize, func() error { err := rsc.podControl.CreatePodsWithControllerRef(rs.Namespace, &rs.Spec.Template, rs, metav1.NewControllerRef(rs, rsc.GroupVersionKind)) // 6、若为 timeout err 则忽略 if err != nil && errors.IsTimeout(err) { return nil } return err }) // 7、计算未创建的 pod 数,并记录在 expectations 中 // 若 pod 创建成功,informer watch 到事件后会在 addPod handler 中更新 expectations if skippedPods := diff - successfulCreations; skippedPods > 0 { for i := 0; i 0 { // 8、若 diff >0 说明需要删除多创建的 pod if diff > rsc.burstReplicas { diff = rsc.burstReplicas } // 9、getPodsToDelete 会按照一定的策略找出需要删除的 pod 列表 podsToDelete := getPodsToDelete(filteredPods, diff) // 10、在 expectations 中进行记录,若该 key 已经存在会进行覆盖 rsc.expectations.ExpectDeletions(rsKey, getPodKeys(podsToDelete)) // 11、进行并发删除的操作 errCh := make(chan error, diff) var wg sync.WaitGroup wg.Add(diff) for _, pod := range podsToDelete { go func(targetPod *v1.Pod) { defer wg.Done() if err := rsc.podControl.DeletePod(rs.Namespace, targetPod.Name, rs); err != nil { podKey := controller.PodKey(targetPod) // 12、某次删除操作若失败会记录在 expectations 中 rsc.expectations.DeletionObserved(rsKey, podKey) errCh slowStartBatch 会批量创建出已计算出的 diff pod 数,创建的 pod 数依次为 1、2、4、8......,呈指数级增长,其方法如下所示: k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:658 func slowStartBatch(count int, initialBatchSize int, fn func() error) (int, error) { remaining := count successes := 0 for batchSize := integer.IntMin(remaining, initialBatchSize); batchSize > 0; batchSize = integer.IntMin(2*batchSize, remaining) { errCh := make(chan error, batchSize) var wg sync.WaitGroup wg.Add(batchSize) for i := 0; i 0 { return successes, 若 diff > 0 时再删除 pod 阶段会调用getPodsToDelete 对 pod 进行筛选操作,此阶段会选出最劣质的 pod,下面是用到的 6 种筛选方法: 1、判断是够绑定了 node:Unassigned 2、判断 pod phase:PodPending 3、判断 pod 状态:Not ready 4、若 pod 都为 ready,则按运行时间排序,运行时间最短会被删除:empty time 5、根据 pod 重启次数排序:higher restart counts 6、按 pod 创建时间进行排序:Empty creation time pods 上面的几个排序规则遵循互斥原则,从上到下进行匹配,符合条件则排序完成,代码如下所示: k8s.io/kubernetes/pkg/controller/replicaset/replica_set.go:684 func getPodsToDelete(filteredPods []*v1.Pod, diff int) []*v1.Pod { if diff k8s.io/kubernetes/pkg/controller/controller_utils.go:735 type ActivePods []*v1.Pod func (s ActivePods) Len() int { return len(s) } func (s ActivePods) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ActivePods) Less(i, j int) bool { // 1. Unassigned maxContainerRestarts(s[j]) } // 6. Empty creation time pods calculateStatus calculateStatus 会通过当前 pod 的状态计算出 rs 中 status 字段值,status 字段如下所示: status: availableReplicas: 10 fullyLabeledReplicas: 10 observedGeneration: 1 readyReplicas: 10 replicas: 10 k8s.io/kubernetes/pkg/controller/replicaset/replica_set_utils.go:85 func calculateStatus(......) apps.ReplicaSetStatus { newStatus := rs.Status fullyLabeledReplicasCount := 0 readyReplicasCount := 0 availableReplicasCount := 0 templateLabel := labels.Set(rs.Spec.Template.Labels).AsSelectorPreValidated() for _, pod := range filteredPods { if templateLabel.Matches(labels.Set(pod.Labels)) { fullyLabeledReplicasCount++ } if podutil.IsPodReady(pod) { readyReplicasCount++ if podutil.IsPodAvailable(pod, rs.Spec.MinReadySeconds, metav1.Now()) { availableReplicasCount++ } } } failureCond := GetCondition(rs.Status, apps.ReplicaSetReplicaFailure) if manageReplicasErr != nil && failureCond == nil { var reason string if diff := len(filteredPods) - int(*(rs.Spec.Replicas)); diff 0 { reason = \"FailedDelete\" } cond := NewReplicaSetCondition(apps.ReplicaSetReplicaFailure, v1.ConditionTrue, reason, manageReplicasErr.Error()) SetCondition(&newStatus, cond) } else if manageReplicasErr == nil && failureCond != nil { RemoveCondition(&newStatus, apps.ReplicaSetReplicaFailure) } newStatus.Replicas = int32(len(filteredPods)) newStatus.FullyLabeledReplicas = int32(fullyLabeledReplicasCount) newStatus.ReadyReplicas = int32(readyReplicasCount) newStatus.AvailableReplicas = int32(availableReplicasCount) return newStatus } expectations 机制 通过上面的分析可知,在 rs 每次入队后进行 sync 操作时,首先需要判断该 rs 是否满足 expectations 机制,那么这个 expectations 的目的是什么?其实,rs 除了有 informer 的缓存外,还有一个本地缓存就是 expectations,expectations 会记录 rs 所有对象需要 add/del 的 pod 数量,若两者都为 0 则说明该 rs 所期望创建的 pod 或者删除的 pod 数已经被满足,若不满足则说明某次在 syncLoop 中创建或者删除 pod 时有失败的操作,则需要等待 expectations 过期后再次同步该 rs。 通过上面对 eventHandler 的分析,再来总结一下触发 replicaSet 对象发生同步事件的条件: 1、与 rs 相关的:AddRS、UpdateRS、DeleteRS; 2、与 pod 相关的:AddPod、UpdatePod、DeletePod; 3、informer 二级缓存的同步; 但是所有的更新事件是否都需要执行 sync 操作?对于除 rs.Spec.Replicas 之外的更新操作其实都没必要执行 sync 操作,因为 spec 其他字段和 status 的更新都不需要创建或者删除 pod。 在 sync 操作真正开始之前,依据 expectations 机制进行判断,确定是否要真正地启动一次 sync,因为在 eventHandler 阶段也会更新 expectations 值,从上面的 eventHandler 中可以看到在 addPod 中会调用 rsc.expectations.CreationObserved 更新 rsKey 的 expectations,将其 add 值 -1,在 deletePod 中调用 rsc.expectations.DeletionObserved 将其 del 值 -1。所以等到 sync 时,若 controllerKey(name 或者 ns/name)满足 expectations 机制则进行 sync 操作,而 updatePod 并不会修改 expectations,所以,expectations 的设计就是当需要创建或删除 pod 才会触发对应的 sync 操作,expectations 机制的目的就是减少不必要的 sync 操作。 什么条件下 expectations 机制会满足? 1、当 expectations 中不存在 rsKey 时,也就说首次创建 rs 时; 2、当 expectations 中 del 以及 add 值都为 0 时,即 rs 所需要创建或者删除的 pod 数都已满足; 3、当 expectations 过期时,即超过 5 分钟未进行 sync 操作; 最后再看一下 expectations 中用到的几个方法: // 创建了一个 pod 说明 expectations 中对应的 key add 期望值需要减少一个 pod, add -1 CreationObserved(controllerKey string) // 删除了一个 pod 说明 expectations 中对应的 key del 期望值需要减少一个 pod, del - 1 DeletionObserved(controllerKey string) // 写入 key 需要 add 的 pod 数量 ExpectCreations(controllerKey string, adds int) error // 写入 key 需要 del 的 pod 数量 ExpectDeletions(controllerKey string, dels int) error // 删除该 key DeleteExpectations(controllerKey string) 当在 syncLoop 中发现满足条件时,会执行 manageReplicas 方法,在 manageReplicas 中无论是为 rs 创建还是删除 pod 都会调用 ExpectCreations 和 ExpectDeletions 为 rsKey 创建 expectations 对象。 总结 本文主要从源码层面分析了 replicaSetController 的设计与实现,但是不得不说其在设计方面考虑了很多因素,文中只提到了笔者理解了或者思考后稍有了解的一些机制,至于其他设计思想还得自行阅读代码体会。 下面以一个流程图总结下创建 rs 的主要流程。 SatisfiedExpectations (expectations 中不存在 rsKey,rsNeedsSync 为 true) | 判断 add/del pod | | | ∨ | 创建 expectations 对象, | 并设置 add/del 值 ∨ | create rs --> syncReplicaSet --> manageReplicas --> ∨ (为 rs 创建 pod) 调用 slowStartBatch 批量创建 pod/ | 删除筛选出的多余 pod | | | ∨ | 更新 expectations 对象 ∨ updateReplicaSetStatus (更新 rs 的 status subResource) 参考: https://keyla.vip/k8s/3-master/controller/replica-set/ Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kube_scheduler_process.html":{"url":"kubernetes/kube_scheduler_process.html","title":"kube-scheduler 源码分析","keywords":"","body":"kube-scheduler 的设计 Kube-scheduler 是 kubernetes 的核心组件之一,也是所有核心组件之间功能比较单一的,其代码也相对容易理解。kube-scheduler 的目的就是为每一个 pod 选择一个合适的 node,整体流程可以概括为三步,获取未调度的 podList,通过执行一系列调度算法为 pod 选择一个合适的 node,提交数据到 apiserver,其核心则是一系列调度算法的设计与执行。 官方对 kube-scheduler 的调度流程描述 The Kubernetes Scheduler: For given pod: +---------------------------------------------+ | Schedulable nodes: | | | | +--------+ +--------+ +--------+ | | | node 1 | | node 2 | | node 3 | | | +--------+ +--------+ +--------+ | | | +-------------------+-------------------------+ | | v +-------------------+-------------------------+ Pred. filters: node 3 doesn't have enough resource +-------------------+-------------------------+ | | v +-------------------+-------------------------+ | remaining nodes: | | +--------+ +--------+ | | | node 1 | | node 2 | | | +--------+ +--------+ | | | +-------------------+-------------------------+ | | v +-------------------+-------------------------+ Priority function: node 1: p=2 node 2: p=5 +-------------------+-------------------------+ | | v select max{node priority} = node 2 kube-scheduler 目前包含两部分调度算法 predicates 和 priorities,首先执行 predicates 算法过滤部分 node 然后执行 priorities 算法为所有 node 打分,最后从所有 node 中选出分数最高的最为最佳的 node。 kube-scheduler 源码分析 kubernetes 版本: v1.16 kubernetes 中所有组件的启动流程都是类似的,首先会解析命令行参数、添加默认值,kube-scheduler 的默认参数在 k8s.io/kubernetes/pkg/scheduler/apis/config/v1alpha1/defaults.go 中定义的。然后会执行 run 方法启动主逻辑,下面直接看 kube-scheduler 的主逻辑 run 方法执行过程。 Run() 方法主要做了以下工作: 初始化 scheduler 对象 启动 kube-scheduler server,kube-scheduler 监听 10251 和 10259 端口,10251 端口不需要认证,可以获取 healthz metrics 等信息,10259 为安全端口,需要认证 启动所有的 informer 执行 sched.Run() 方法,执行主调度逻辑 k8s.io/kubernetes/cmd/kube-scheduler/app/server.go:160 func Run(cc schedulerserverconfig.CompletedConfig, stopCh 下面看一下 scheduler.New() 方法是如何初始化 scheduler 结构体的,该方法主要的功能是初始化默认的调度算法以及默认的调度器 GenericScheduler。 创建 scheduler 配置文件 根据默认的 DefaultProvider 初始化 schedulerAlgorithmSource 然后加载默认的预选及优选算法,然后初始化 GenericScheduler 若启动参数提供了 policy config 则使用其覆盖默认的预选及优选算法并初始化 GenericScheduler,不过该参数现已被弃用 k8s.io/kubernetes/pkg/scheduler/scheduler.go:166 func New(......) (*Scheduler, error) { ...... // 1、创建 scheduler 的配置文件 configurator := factory.NewConfigFactory(&factory.ConfigFactoryArgs{ ...... }) var config *factory.Config source := schedulerAlgorithmSource // 2、加载默认的调度算法 switch { case source.Provider != nil: // 使用默认的 ”DefaultProvider“ 初始化 config sc, err := configurator.CreateFromProvider(*source.Provider) if err != nil { return nil, fmt.Errorf(\"couldn't create scheduler using provider %q: %v\", *source.Provider, err) } config = sc case source.Policy != nil: // 通过启动时指定的 policy source 加载 config ...... config = sc default: return nil, fmt.Errorf(\"unsupported algorithm source: %v\", source) } // Additional tweaks to the config produced by the configurator. config.Recorder = recorder config.DisablePreemption = options.disablePreemption config.StopEverything = stopCh // 3.创建 scheduler 对象 sched := NewFromConfig(config) ...... return sched, nil } 下面是 pod informer 的启动逻辑,只监听 status.phase 不为 succeeded 以及 failed 状态的 pod,即非 terminating 的 pod。 k8s.io/kubernetes/pkg/scheduler/factory/factory.go:527 func NewPodInformer(client clientset.Interface, resyncPeriod time.Duration) coreinformers.PodInformer { selector := fields.ParseSelectorOrDie( \"status.phase!=\" + string(v1.PodSucceeded) + \",status.phase!=\" + string(v1.PodFailed)) lw := cache.NewListWatchFromClient(client.CoreV1().RESTClient(), string(v1.ResourcePods), metav1.NamespaceAll, selector) return &podInformer{ informer: cache.NewSharedIndexInformer(lw, &v1.Pod{}, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}), } } 然后继续看 Run() 方法中最后执行的 sched.Run() 调度循环逻辑,若 informer 中的 cache 同步完成后会启动一个循环逻辑执行 sched.scheduleOne 方法。 k8s.io/kubernetes/pkg/scheduler/scheduler.go:313 func (sched *Scheduler) Run() { if !sched.config.WaitForCacheSync() { return } go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything) } scheduleOne() 每次对一个 pod 进行调度,主要有以下步骤: 从 scheduler 调度队列中取出一个 pod,如果该 pod 处于删除状态则跳过 执行调度逻辑 sched.schedule() 返回通过预算及优选算法过滤后选出的最佳 node 如果过滤算法没有选出合适的 node,则返回 core.FitError 若没有合适的 node 会判断是否启用了抢占策略,若启用了则执行抢占机制 判断是否需要 VolumeScheduling 特性 执行 reserve plugin pod 对应的 spec.NodeName 写上 scheduler 最终选择的 node,更新 scheduler cache 请求 apiserver 异步处理最终的绑定操作,写入到 etcd 执行 permit plugin 执行 prebind plugin 执行 postbind plugin k8s.io/kubernetes/pkg/scheduler/scheduler.go:515 func (sched *Scheduler) scheduleOne() { fwk := sched.Framework pod := sched.NextPod() if pod == nil { return } // 1.判断 pod 是否处于删除状态 if pod.DeletionTimestamp != nil { ...... } // 2.执行调度策略选择 node start := time.Now() pluginContext := framework.NewPluginContext() scheduleResult, err := sched.schedule(pod, pluginContext) if err != nil { if fitError, ok := err.(*core.FitError); ok { // 3.若启用抢占机制则执行 if sched.DisablePreemption { ...... } else { preemptionStartTime := time.Now() sched.preempt(pluginContext, fwk, pod, fitError) ...... } ...... metrics.PodScheduleFailures.Inc() } else { klog.Errorf(\"error selecting node for pod: %v\", err) metrics.PodScheduleErrors.Inc() } return } ...... assumedPod := pod.DeepCopy() // 4.判断是否需要 VolumeScheduling 特性 allBound, err := sched.assumeVolumes(assumedPod, scheduleResult.SuggestedHost) if err != nil { klog.Errorf(\"error assuming volumes: %v\", err) metrics.PodScheduleErrors.Inc() return } // 5.执行 \"reserve\" plugins if sts := fwk.RunReservePlugins(pluginContext, assumedPod, scheduleResult.SuggestedHost); !sts.IsSuccess() { ..... } // 6.为 pod 设置 NodeName 字段,更新 scheduler 缓存 err = sched.assume(assumedPod, scheduleResult.SuggestedHost) if err != nil { ...... } // 7.异步请求 apiserver go func() { // Bind volumes first before Pod if !allBound { err := sched.bindVolumes(assumedPod) if err != nil { ...... return } } // 8.执行 \"permit\" plugins permitStatus := fwk.RunPermitPlugins(pluginContext, assumedPod, scheduleResult.SuggestedHost) if !permitStatus.IsSuccess() { ...... } // 9.执行 \"prebind\" plugins preBindStatus := fwk.RunPreBindPlugins(pluginContext, assumedPod, scheduleResult.SuggestedHost) if !preBindStatus.IsSuccess() { ...... } err := sched.bind(assumedPod, scheduleResult.SuggestedHost, pluginContext) ...... if err != nil { ...... } else { ...... // 10.执行 \"postbind\" plugins fwk.RunPostBindPlugins(pluginContext, assumedPod, scheduleResult.SuggestedHost) } }() } scheduleOne() 中通过调用 sched.schedule() 来执行预选与优选算法处理: k8s.io/kubernetes/pkg/scheduler/scheduler.go:337 func (sched *Scheduler) schedule(pod *v1.Pod, pluginContext *framework.PluginContext) (core.ScheduleResult, error) { result, err := sched.Algorithm.Schedule(pod, pluginContext) if err != nil { ...... } return result, err } sched.Algorithm 是一个 interface,主要包含四个方法,GenericScheduler 是其具体的实现: k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:131 type ScheduleAlgorithm interface { Schedule(*v1.Pod, *framework.PluginContext) (scheduleResult ScheduleResult, err error) Preempt(*framework.PluginContext, *v1.Pod, error) (selectedNode *v1.Node, preemptedPods []*v1.Pod, cleanupNominatedPods []*v1.Pod, err error) Predicates() map[string]predicates.FitPredicate Prioritizers() []priorities.PriorityConfig } Schedule():正常调度逻辑,包含预算与优选算法的执行 Preempt():抢占策略,在 pod 调度发生失败的时候尝试抢占低优先级的 pod,函数返回发生抢占的 node,被 抢占的 pods 列表,nominated node name 需要被移除的 pods 列表以及 error Predicates():predicates 算法列表 Prioritizers():prioritizers 算法列表 kube-scheduler 提供的默认调度为 DefaultProvider,DefaultProvider 配置的 predicates 和 priorities policies 在 k8s.io/kubernetes/pkg/scheduler/algorithmprovider/defaults/defaults.go 中定义,算法具体实现是在 k8s.io/kubernetes/pkg/scheduler/algorithm/predicates/ 和k8s.io/kubernetes/pkg/scheduler/algorithm/priorities/ 中,默认的算法如下所示: pkg/scheduler/algorithmprovider/defaults/defaults.go func defaultPredicates() sets.String { return sets.NewString( predicates.NoVolumeZoneConflictPred, predicates.MaxEBSVolumeCountPred, predicates.MaxGCEPDVolumeCountPred, predicates.MaxAzureDiskVolumeCountPred, predicates.MaxCSIVolumeCountPred, predicates.MatchInterPodAffinityPred, predicates.NoDiskConflictPred, predicates.GeneralPred, predicates.CheckNodeMemoryPressurePred, predicates.CheckNodeDiskPressurePred, predicates.CheckNodePIDPressurePred, predicates.CheckNodeConditionPred, predicates.PodToleratesNodeTaintsPred, predicates.CheckVolumeBindingPred, ) } func defaultPriorities() sets.String { return sets.NewString( priorities.SelectorSpreadPriority, priorities.InterPodAffinityPriority, priorities.LeastRequestedPriority, priorities.BalancedResourceAllocation, priorities.NodePreferAvoidPodsPriority, priorities.NodeAffinityPriority, priorities.TaintTolerationPriority, priorities.ImageLocalityPriority, ) } 下面继续看 sched.Algorithm.Schedule() 调用具体调度算法的过程: 检查 pod pvc 信息 执行 prefilter plugins 获取 scheduler cache 的快照,每次调度 pod 时都会获取一次快照 执行 g.findNodesThatFit() 预选算法 执行 postfilter plugin 若 node 为 0 直接返回失败的 error,若 node 数为1 直接返回该 node 执行 g.priorityMetaProducer() 获取 metaPrioritiesInterface,计算 pod 的metadata,检查该 node 上是否有相同 meta 的 pod 执行 PrioritizeNodes() 算法 执行 g.selectHost() 通过得分选择一个最佳的 node k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:186 func (g *genericScheduler) Schedule(pod *v1.Pod, pluginContext *framework.PluginContext) (result ScheduleResult, err error) { ...... // 1.检查 pod pvc if err := podPassesBasicChecks(pod, g.pvcLister); err != nil { return result, err } // 2.执行 \"prefilter\" plugins preFilterStatus := g.framework.RunPreFilterPlugins(pluginContext, pod) if !preFilterStatus.IsSuccess() { return result, preFilterStatus.AsError() } // 3.获取 node 数量 numNodes := g.cache.NodeTree().NumNodes() if numNodes == 0 { return result, ErrNoNodesAvailable } // 4.快照 node 信息 if err := g.snapshot(); err != nil { return result, err } // 5.执行预选算法 startPredicateEvalTime := time.Now() filteredNodes, failedPredicateMap, filteredNodesStatuses, err := g.findNodesThatFit(pluginContext, pod) if err != nil { return result, err } // 6.执行 \"postfilter\" plugins postfilterStatus := g.framework.RunPostFilterPlugins(pluginContext, pod, filteredNodes, filteredNodesStatuses) if !postfilterStatus.IsSuccess() { return result, postfilterStatus.AsError() } // 7.预选后没有合适的 node 直接返回 if len(filteredNodes) == 0 { ...... } startPriorityEvalTime := time.Now() // 8.若只有一个 node 则直接返回该 node if len(filteredNodes) == 1 { return ScheduleResult{ SuggestedHost: filteredNodes[0].Name, EvaluatedNodes: 1 + len(failedPredicateMap), FeasibleNodes: 1, }, nil } // 9.获取 pod meta 信息,执行优选算法 metaPrioritiesInterface := g.priorityMetaProducer(pod, g.nodeInfoSnapshot.NodeInfoMap) priorityList, err := PrioritizeNodes(pod, g.nodeInfoSnapshot.NodeInfoMap, metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders, g.framework, pluginContext) if err != nil { return result, err } // 10.根据打分选择最佳的 node host, err := g.selectHost(priorityList) trace.Step(\"Selecting host done\") return ScheduleResult{ SuggestedHost: host, EvaluatedNodes: len(filteredNodes) + len(failedPredicateMap), FeasibleNodes: len(filteredNodes), }, err } 至此,scheduler 的整个过程分析完毕。 总结 本文主要对于 kube-scheduler v1.16 的调度流程进行了分析,但其中有大量的细节都暂未提及,包括预选算法以及优选算法的具体实现、优先级与抢占调度的实现、framework 的使用及实现,因篇幅有限,部分内容会在后文继续说明。 参考: The Kubernetes Scheduler scheduling design proposals Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kube_scheduler_algorithm.html":{"url":"kubernetes/kube_scheduler_algorithm.html","title":"kube-scheduler predicates 与 priorities 调度算法源码分析","keywords":"","body":"在上篇文章kube-scheduler 源码分析中已经介绍了 kube-scheduler 的设计以及从源码角度分析了其执行流程,这篇文章会专注介绍调度过程中 predicates 和 priorities 这两个调度策略主要发生作用的阶段。 kubernetes 版本: v1.16 predicates 调度算法源码分析 predicates 算法主要是对集群中的 node 进行过滤,选出符合当前 pod 运行的 nodes。 调度算法说明 上节已经提到默认的调度算法在pkg/scheduler/algorithmprovider/defaults/defaults.go中定义了: func defaultPredicates() sets.String { return sets.NewString( predicates.NoVolumeZoneConflictPred, predicates.MaxEBSVolumeCountPred, predicates.MaxGCEPDVolumeCountPred, predicates.MaxAzureDiskVolumeCountPred, predicates.MaxCSIVolumeCountPred, predicates.MatchInterPodAffinityPred, predicates.NoDiskConflictPred, predicates.GeneralPred, predicates.CheckNodeMemoryPressurePred, predicates.CheckNodeDiskPressurePred, predicates.CheckNodePIDPressurePred, predicates.CheckNodeConditionPred, predicates.PodToleratesNodeTaintsPred, predicates.CheckVolumeBindingPred, ) } 下面是对默认调度算法的一些说明: predicates 算法 说明 GeneralPred GeneralPred 包含 PodFitsResources、PodFitsHost,、PodFitsHostPorts、PodMatchNodeSelector 四种算法 NoDiskConflictPred 检查多个 Pod 声明挂载的持久化 Volume 是否有冲突 MaxGCEPDVolumeCountPred 检查 GCE 持久化 Volume 是否超过了一定数目 MaxAzureDiskVolumeCountPred 检查 Azure 持久化 Volume 是否超过了一定数目 MaxCSIVolumeCountPred 检查 CSI 持久化 Volume 是否超过了一定数目(已废弃) MaxEBSVolumeCountPred 检查 EBS 持久化 Volume 是否超过了一定数目 NoVolumeZoneConflictPred 检查持久化 Volume 的 Zone(高可用域)标签是否与节点的 Zone 标签相匹配 CheckVolumeBindingPred 检查该 Pod 对应 PV 的 nodeAffinity 字段是否跟某个节点的标签相匹配,Local Persistent Volume(本地持久化卷)必须使用 nodeAffinity 来跟某个具体的节点绑定 PodToleratesNodeTaintsPred 检查 Node 的 Taint 机制,只有当 Pod 的 Toleration 字段与 Node 的 Taint 字段能够匹配时,这个 Pod 才能被调度到该节点上 MatchInterPodAffinityPred 检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系 CheckNodeConditionPred 检查 NodeCondition CheckNodePIDPressurePred 检查 NodePIDPressure CheckNodeDiskPressurePred 检查 NodeDiskPressure CheckNodeMemoryPressurePred 检查 NodeMemoryPressure 默认的 predicates 调度算法主要分为五种类型: 1、第一种类型叫作 GeneralPredicates,包含 PodFitsResources、PodFitsHost、PodFitsHostPorts、PodMatchNodeSelector 四种策略,其具体含义如下所示: PodFitsHost:检查宿主机的名字是否跟 Pod 的 spec.nodeName 一致 PodFitsHostPorts:检查 Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突 PodMatchNodeSelector:检查 Pod 的 nodeSelector 或者 nodeAffinity 指定的节点是否与节点匹配等 PodFitsResources:检查主机的资源是否满足 Pod 的需求,根据实际已经分配(Request)的资源量做调度 kubelet 在启动 Pod 前,会执行一个 Admit 操作来进行二次确认,这里二次确认的规则就是执行一遍 GeneralPredicates。 2、第二种类型是与 Volume 相关的过滤规则,主要有NoDiskConflictPred、MaxGCEPDVolumeCountPred、MaxAzureDiskVolumeCountPred、MaxCSIVolumeCountPred、MaxEBSVolumeCountPred、NoVolumeZoneConflictPred、CheckVolumeBindingPred。 3、第三种类型是宿主机相关的过滤规则,主要是 PodToleratesNodeTaintsPred。 4、第四种类型是 Pod 相关的过滤规则,主要是 MatchInterPodAffinityPred。 5、第五种类型是新增的过滤规则,与宿主机的运行状况有关,主要有 CheckNodeCondition、 CheckNodeMemoryPressure、CheckNodePIDPressure、CheckNodeDiskPressure 四种。若启用了 TaintNodesByCondition FeatureGates 则在 predicates 算法中会将该四种算法移除,TaintNodesByCondition 基于 node conditions 当 node 出现 pressure 时自动为 node 打上 taints 标签,该功能在 v1.8 引入,v1.12 成为 beta 版本,目前 v1.16 中也是 beta 版本,但在 v1.13 中该功能已默认启用。 predicates 调度算法也有一个顺序,要不然在一台资源已经严重不足的宿主机上,上来就开始计算 PodAffinityPredicate 是没有实际意义的,其默认顺序如下所示: k8s.io/kubernetes/pkg/scheduler/algorithm/predicates/predicates.go:146 var ( predicatesOrdering = []string{CheckNodeConditionPred, CheckNodeUnschedulablePred, GeneralPred, HostNamePred, PodFitsHostPortsPred, MatchNodeSelectorPred, PodFitsResourcesPred, NoDiskConflictPred, PodToleratesNodeTaintsPred, PodToleratesNodeNoExecuteTaintsPred, CheckNodeLabelPresencePred, CheckServiceAffinityPred, MaxEBSVolumeCountPred, MaxGCEPDVolumeCountPred, MaxCSIVolumeCountPred, MaxAzureDiskVolumeCountPred, MaxCinderVolumeCountPred, CheckVolumeBindingPred, NoVolumeZoneConflictPred, CheckNodeMemoryPressurePred, CheckNodePIDPressurePred, CheckNodeDiskPressurePred, EvenPodsSpreadPred, MatchInterPodAffinityPred} ) 源码分析 上节中已经说到调用预选以及优选算法的逻辑在 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:189中, func (g *genericScheduler) Schedule(pod *v1.Pod, pluginContext *framework.PluginContext) (result ScheduleResult, err error) { ...... // 执行 predicates 策略 filteredNodes, failedPredicateMap, filteredNodesStatuses, err := g.findNodesThatFit(pluginContext, pod) ...... // 执行 priorities 策略 priorityList, err := PrioritizeNodes(pod, g.nodeInfoSnapshot.NodeInfoMap, metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders, g.framework, pluginContext) ...... return } findNodesThatFit() 是 predicates 策略的实际调用方法,其基本流程如下: 设定最多需要检查的节点数,作为预选节点数组的容量,避免总节点过多影响调度效率 通过NodeTree()不断获取下一个节点来判断该节点是否满足 pod 的调度条件 通过之前注册的各种 predicates 函数来判断当前节点是否符合 pod 的调度条件 最后返回满足调度条件的 node 列表,供下一步的优选操作 checkNode()是一个校验 node 是否符合要求的函数,其实际调用到的核心函数是podFitsOnNode(),再通过workqueue() 并发执行checkNode() 函数,workqueue() 会启动 16 个 goroutine 来并行计算需要筛选的 node 列表,其主要流程如下: 通过 cache 中的 NodeTree() 不断获取下一个 node 将当前 node 和 pod 传入podFitsOnNode() 方法中来判断当前 node 是否符合要求 如果当前 node 符合要求就将当前 node 加入预选节点的数组中filtered 如果当前 node 不满足要求,则加入到失败的数组中,并记录原因 通过workqueue.ParallelizeUntil()并发执行checkNode()函数,一旦找到足够的可行节点数后就停止筛选更多节点 若配置了 extender 则再次进行过滤已筛选出的 node k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:464 func (g *genericScheduler) findNodesThatFit(pluginContext *framework.PluginContext, pod *v1.Pod) ([]*v1.Node, FailedPredicateMap, framework.NodeToStatusMap, error) { var filtered []*v1.Node failedPredicateMap := FailedPredicateMap{} filteredNodesStatuses := framework.NodeToStatusMap{} if len(g.predicates) == 0 { filtered = g.cache.ListNodes() } else { allNodes := int32(g.cache.NodeTree().NumNodes()) // 1.设定最多需要检查的节点数 numNodesToFind := g.numFeasibleNodesToFind(allNodes) filtered = make([]*v1.Node, numNodesToFind) ...... // 2.获取该 pod 的 meta 值 meta := g.predicateMetaProducer(pod, g.nodeInfoSnapshot.NodeInfoMap) // 3.checkNode 为执行预选算法的函数 checkNode := func(i int) { nodeName := g.cache.NodeTree().Next() // 4.podFitsOnNode 最终执行预选算法的函数 fits, failedPredicates, status, err := g.podFitsOnNode( ...... ) if err != nil { ...... } if fits { length := atomic.AddInt32(&filteredLen, 1) if length > numNodesToFind { cancel() atomic.AddInt32(&filteredLen, -1) } else { filtered[length-1] = g.nodeInfoSnapshot.NodeInfoMap[nodeName].Node() } } else { ...... } } // 5.启动 16 个 goroutine 并发执行 checkNode 函数 workqueue.ParallelizeUntil(ctx, 16, int(allNodes), checkNode) filtered = filtered[:filteredLen] if len(errs) > 0 { ...... } } // 6.若配置了 extender 则再次进行过滤 if len(filtered) > 0 && len(g.extenders) != 0 { ...... } return filtered, failedPredicateMap, filteredNodesStatuses, nil } 然后继续看如何设定最多需要检查的节点数,此过程由numFeasibleNodesToFind()进行处理,基本流程如下: 如果总的 node 节点小于minFeasibleNodesToFind(默认为100)则直接返回总节点数 如果节点数超过 100,则取指定百分比 percentageOfNodesToScore(默认值为 50)的节点数 ,当该百分比后的数目仍小于minFeasibleNodesToFind,则返回minFeasibleNodesToFind 如果百分比后的数目大于minFeasibleNodesToFind,则返回该百分比的节点数 所以当节点数小于 100 时直接返回,大于 100 时只返回其总数的 50%。percentageOfNodesToScore 参数在 v1.12 引入,默认值为 50,kube-scheduler 在启动时可以设定该参数的值。 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:441 func (g *genericScheduler) numFeasibleNodesToFind(numAllNodes int32) (numNodes int32) { if numAllNodes = 100 { return numAllNodes } adaptivePercentage := g.percentageOfNodesToScore if adaptivePercentage pridicates 调度算法的核心是 podFitsOnNode() ,scheduler 的抢占机制也会执行该函数,podFitsOnNode()基本流程如下: 遍历已经注册好的预选策略predicates.Ordering(),按顺序执行对应的策略函数 遍历执行每个策略函数,并返回是否合适,预选失败的原因和错误 如果预选函数执行失败,则加入预选失败的数组中,直接返回,后面的预选函数不会再执行 如果该 node 上存在 nominated pod 则执行两次预选函数 因为引入了抢占机制,此处主要说明一下执行两次预选函数的原因: 第一次循环,若该 pod 为抢占者(nominatedPods),调度器会假设该 pod 已经运行在这个节点上,然后更新meta和nodeInfo,nominatedPods是指执行了抢占机制且已经分配到了 node(pod.Status.NominatedNodeName 已被设定) 但是还没有真正运行起来的 pod,然后再执行所有的预选函数。 第二次循环,不将nominatedPods加入到 node 内。 而只有这两遍 predicates 算法都能通过时,这个 pod 和 node 才会被认为是可以绑定(bind)的。这样做是因为考虑到 pod affinity 等策略的执行,如果当前的 pod 与nominatedPods有依赖关系就会有问题,因为nominatedPods不能保证一定可以调度且在已指定的 node 运行成功,也可能出现被其他高优先级的 pod 抢占等问题,关于抢占问题下篇会详细介绍。 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:610 func (g *genericScheduler) podFitsOnNode(......) (bool, []predicates.PredicateFailureReason, *framework.Status, error) { var failedPredicates []predicates.PredicateFailureReason var status *framework.Status podsAdded := false for i := 0; i 至此,关于 predicates 调度算法的执行过程已经分析完。 priorities 调度算法源码分析 priorities 调度算法是在 pridicates 算法后执行的,主要功能是对已经过滤出的 nodes 进行打分并选出最佳的一个 node。 调度算法说明 默认的调度算法在pkg/scheduler/algorithmprovider/defaults/defaults.go中定义了: func defaultPriorities() sets.String { return sets.NewString( priorities.SelectorSpreadPriority, priorities.InterPodAffinityPriority, priorities.LeastRequestedPriority, priorities.BalancedResourceAllocation, priorities.NodePreferAvoidPodsPriority, priorities.NodeAffinityPriority, priorities.TaintTolerationPriority, priorities.ImageLocalityPriority, ) } 默认调度算法的一些说明: priorities 算法 说明 SelectorSpreadPriority 按 service,rs,statefulset 归属计算 Node 上分布最少的同类 Pod数量,数量越少得分越高,默认权重为1 InterPodAffinityPriority pod 亲和性选择策略,默认权重为1 LeastRequestedPriority 选择空闲资源(CPU 和 Memory)最多的节点,默认权重为1,其计算方式为:score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2 BalancedResourceAllocation CPU、Memory 以及 Volume 资源分配最均衡的节点,默认权重为1,其计算方式为:score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10 NodePreferAvoidPodsPriority 判断 node annotation 是否有scheduler.alpha.kubernetes.io/preferAvoidPods 标签,类似于 taints 机制,过滤标签中定义类型的 pod,默认权重为10000 NodeAffinityPriority 节点亲和性选择策略,默认权重为1 TaintTolerationPriority Pod 是否容忍节点上的 Taint,优先调度到标记了 Taint 的节点,默认权重为1 ImageLocalityPriority 待调度 Pod 需要使用的镜像是否存在于该节点,默认权重为1 源码分析 执行 priorities 调度算法的逻辑是在 PrioritizeNodes()函数中,其目的是执行每个 priority 函数为 node 打分,分数为 0-10,其功能主要有: PrioritizeNodes() 通过并行运行各个优先级函数来对节点进行打分 每个优先级函数会给节点打分,打分范围为 0-10 分,0 表示优先级最低的节点,10表示优先级最高的节点 每个优先级函数有各自的权重 优先级函数返回的节点分数乘以权重以获得加权分数 最后计算所有节点的总加权分数 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:691 func PrioritizeNodes(......) (schedulerapi.HostPriorityList, error) { // 1.检查是否有自定义配置 if len(priorityConfigs) == 0 && len(extenders) == 0 { result := make(schedulerapi.HostPriorityList, 0, len(nodes)) for i := range nodes { hostPriority, err := EqualPriorityMap(pod, meta, nodeNameToInfo[nodes[i].Name]) if err != nil { return nil, err } result = append(result, hostPriority) } return result, nil } ...... results := make([]schedulerapi.HostPriorityList, len(priorityConfigs), len(priorityConfigs)) ...... // 2.使用 workqueue 启动 16 个 goroutine 并发为 node 打分 workqueue.ParallelizeUntil(context.TODO(), 16, len(nodes), func(index int) { nodeInfo := nodeNameToInfo[nodes[index].Name] for i := range priorityConfigs { if priorityConfigs[i].Function != nil { continue } var err error results[i][index], err = priorityConfigs[i].Map(pod, meta, nodeInfo) if err != nil { appendError(err) results[i][index].Host = nodes[index].Name } } }) // 3.执行自定义配置 for i := range priorityConfigs { ...... } wg.Wait() if len(errs) != 0 { return schedulerapi.HostPriorityList{}, errors.NewAggregate(errs) } // 4.运行 Score plugins scoresMap, scoreStatus := framework.RunScorePlugins(pluginContext, pod, nodes) if !scoreStatus.IsSuccess() { return schedulerapi.HostPriorityList{}, scoreStatus.AsError() } result := make(schedulerapi.HostPriorityList, 0, len(nodes)) // 5.为每个 node 汇总分数 for i := range nodes { result = append(result, schedulerapi.HostPriority{Host: nodes[i].Name, Score: 0}) for j := range priorityConfigs { result[i].Score += results[j][i].Score * priorityConfigs[j].Weight } for j := range scoresMap { result[i].Score += scoresMap[j][i].Score } } // 6.执行 extender if len(extenders) != 0 && nodes != nil { ...... } ...... return result, nil } 总结 本文主要讲述了 kube-scheduler 中的 predicates 调度算法与 priorities 调度算法的执行流程,可以看到 kube-scheduler 中有许多的调度策略,但是想要添加自己的策略并不容易,scheduler 目前已经朝着提升性能与扩展性的方向演进了,其调度部分进行性能优化的一个最根本原则就是尽最大可能将集群信息 cache 化,以便从根本上提高 predicates 和 priorities 调度算法的执行效率。第二个就是在 bind 阶段进行异步处理,只会更新其 cache 里的 pod 和 node 的信息,这种基于“乐观”假设的 API 对象更新方式,在 kubernetes 里被称作 assume,如果这次异步的 bind 过程失败了,其实也没有太大关系,等 scheduler cache 同步之后一切又恢复正常了。除了上述的“cache 化”和“乐观绑定”,还有一个重要的设计,那就是“无锁化”,predicates 调度算法与 priorities 调度算法的执行都是并行的,只有在调度队列和 scheduler cache 进行操作时,才需要加锁,而对调度队列的操作并不影响主流程。 参考: https://kubernetes.io/docs/concepts/configuration/scheduling-framework/ predicates-ordering.md Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kube_scheduler_preempt.html":{"url":"kubernetes/kube_scheduler_preempt.html","title":"kube-scheduler 优先级与抢占机制源码分析","keywords":"","body":"前面已经分析了 kube-scheduler 的代码逻辑以及 predicates 与 priorities 算法,本节会继续讲 scheduler 中的一个重要机制,pod 优先级与抢占机制(Pod Priority and Preemption),该功能是在 v1.8 中引入的,v1.11 中该功能为 beta 版本且默认启用了,v1.14 为 stable 版本。 kube-scheduler 源码分析 kube-scheduler predicates 与 priorities 调度算法源码分析 为什么要有优先级与抢占机制 正常情况下,当一个 pod 调度失败后,就会被暂时 “搁置” 处于 pending 状态,直到 pod 被更新或者集群状态发生变化,调度器才会对这个 pod 进行重新调度。但在实际的业务场景中会存在在线与离线业务之分,若在线业务的 pod 因资源不足而调度失败时,此时就需要离线业务下掉一部分为在线业务提供资源,即在线业务要抢占离线业务的资源,此时就需要 scheduler 的优先级和抢占机制了,该机制解决的是 pod 调度失败时该怎么办的问题,若该 pod 的优先级比较高此时并不会被”搁置”,而是会”挤走”某个 node 上的一些低优先级的 pod,这样就可以保证高优先级的 pod 调度成功。 优先级与抢占机制源码分析 kubernetes 版本: v1.16 抢占发生的原因,一定是一个高优先级的 pod 调度失败,我们称这个 pod 为“抢占者”,称被抢占的 pod 为“牺牲者”(victims)。而 kubernetes 调度器实现抢占算法的一个最重要的设计,就是在调度队列的实现里,使用了两个不同的队列。 第一个队列叫作 activeQ,凡是在 activeQ 里的 pod,都是下一个调度周期需要调度的对象。所以,当你在 kubernetes 集群里新创建一个 pod 的时候,调度器会将这个 pod 入队到 activeQ 里面,调度器不断从队列里出队(pop)一个 pod 进行调度,实际上都是从 activeQ 里出队的。 第二个队列叫作 unschedulableQ,专门用来存放调度失败的 pod,当一个 unschedulableQ 里的 pod 被更新之后,调度器会自动把这个 pod 移动到 activeQ 里,从而给这些调度失败的 pod “重新做人”的机会。 当 pod 拥有了优先级之后,高优先级的 pod 就可能会比低优先级的 pod 提前出队,从而尽早完成调度过程。 k8s.io/kubernetes/pkg/scheduler/internal/queue/scheduling_queue.go // NewSchedulingQueue initializes a priority queue as a new scheduling queue. func NewSchedulingQueue(stop 前面的文章已经说了 scheduleOne() 是执行调度算法的主逻辑,其主要功能有: 调用 sched.schedule(),即执行 predicates 算法和 priorities 算法 若执行失败,会返回 core.FitError 若开启了抢占机制,则执行抢占机制 ...... k8s.io/kubernetes/pkg/scheduler/scheduler.go:516 func (sched *Scheduler) scheduleOne() { ...... scheduleResult, err := sched.schedule(pod, pluginContext) // predicates 算法和 priorities 算法执行失败 if err != nil { if fitError, ok := err.(*core.FitError); ok { // 是否开启抢占机制 if sched.DisablePreemption { ....... } else { // 执行抢占机制 preemptionStartTime := time.Now() sched.preempt(pluginContext, fwk, pod, fitError) ...... } ...... } else { ...... } return } ...... } 我们主要来看其中的抢占机制,sched.preempt() 是执行抢占机制的主逻辑,主要功能有: 从 apiserver 获取 pod info 调用 sched.Algorithm.Preempt()执行抢占逻辑,该函数会返回抢占成功的 node、被抢占的 pods(victims) 以及需要被移除已提名的 pods 更新 scheduler 缓存,为抢占者绑定 nodeName,即设定 pod.Status.NominatedNodeName 将 pod info 提交到 apiserver 删除被抢占的 pods 删除被抢占 pods 的 NominatedNodeName 字段 可以看到当上述抢占过程发生时,抢占者并不会立刻被调度到被抢占的 node 上,调度器只会将抢占者的 status.nominatedNodeName 字段设置为被抢占的 node 的名字。然后,抢占者会重新进入下一个调度周期,在新的调度周期里来决定是不是要运行在被抢占的节点上,当然,即使在下一个调度周期,调度器也不会保证抢占者一定会运行在被抢占的节点上。 这样设计的一个重要原因是调度器只会通过标准的 DELETE API 来删除被抢占的 pod,所以,这些 pod 必然是有一定的“优雅退出”时间(默认是 30s)的。而在这段时间里,其他的节点也是有可能变成可调度的,或者直接有新的节点被添加到这个集群中来。所以,鉴于优雅退出期间集群的可调度性可能会发生的变化,把抢占者交给下一个调度周期再处理,是一个非常合理的选择。而在抢占者等待被调度的过程中,如果有其他更高优先级的 pod 也要抢占同一个节点,那么调度器就会清空原抢占者的 status.nominatedNodeName 字段,从而允许更高优先级的抢占者执行抢占,并且,这也使得原抢占者本身也有机会去重新抢占其他节点。以上这些都是设置 nominatedNodeName 字段的主要目的。 k8s.io/kubernetes/pkg/scheduler/scheduler.go:352 func (sched *Scheduler) preempt(pluginContext *framework.PluginContext, fwk framework.Framework, preemptor *v1.Pod, scheduleErr error) (string, error) { // 获取 pod info preemptor, err := sched.PodPreemptor.GetUpdatedPod(preemptor) if err != nil { klog.Errorf(\"Error getting the updated preemptor pod object: %v\", err) return \"\", err } // 执行抢占算法 node, victims, nominatedPodsToClear, err := sched.Algorithm.Preempt(pluginContext, preemptor, scheduleErr) if err != nil { ...... } var nodeName = \"\" if node != nil { nodeName = node.Name // 更新 scheduler 缓存,为抢占者绑定 nodename,即设定 pod.Status.NominatedNodeName sched.SchedulingQueue.UpdateNominatedPodForNode(preemptor, nodeName) // 将 pod info 提交到 apiserver err = sched.PodPreemptor.SetNominatedNodeName(preemptor, nodeName) if err != nil { sched.SchedulingQueue.DeleteNominatedPodIfExists(preemptor) return \"\", err } // 删除被抢占的 pods for _, victim := range victims { if err := sched.PodPreemptor.DeletePod(victim); err != nil { return \"\", err } ...... } } // 删除被抢占 pods 的 NominatedNodeName 字段 for _, p := range nominatedPodsToClear { rErr := sched.PodPreemptor.RemoveNominatedNodeName(p) if rErr != nil { ...... } } return nodeName, err } preempt()中会调用 sched.Algorithm.Preempt()来执行实际抢占的算法,其主要功能有: 判断 err 是否为 FitError 调用podEligibleToPreemptOthers()确认 pod 是否有抢占其他 pod 的资格,若 pod 已经抢占了低优先级的 pod,被抢占的 pod 处于 terminating 状态中,则不会继续进行抢占 如果确定抢占可以发生,调度器会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程 过滤预选失败的 node 列表,此处会检查 predicates 失败的原因,若存在 NodeSelectorNotMatch、PodNotMatchHostName 这些 error 则不能成为抢占者,如果过滤出的候选 node 为空则返回抢占者作为 nominatedPodsToClear 获取 PodDisruptionBudget 对象 从预选失败的 node 列表中并发计算可以被抢占的 nodes,得到 nodeToVictims 若声明了 extenders 则调用 extenders 再次过滤 nodeToVictims 调用 pickOneNodeForPreemption() 从 nodeToVictims 中选出一个节点作为最佳候选人 移除低优先级 pod 的 Nominated,更新这些 pod,移动到 activeQ 队列中,让调度器为这些 pod 重新 bind node k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:320 func (g *genericScheduler) Preempt(pluginContext *framework.PluginContext, pod *v1.Pod, scheduleErr error) (*v1.Node, []*v1.Pod, []*v1.Pod, error) { fitError, ok := scheduleErr.(*FitError) if !ok || fitError == nil { return nil, nil, nil, nil } // 判断 pod 是否支持抢占,若 pod 已经抢占了低优先级的 pod,被抢占的 pod 处于 terminating 状态中,则不会继续进行抢占 if !podEligibleToPreemptOthers(pod, g.nodeInfoSnapshot.NodeInfoMap, g.enableNonPreempting) { return nil, nil, nil, nil } // 从缓存中获取 node list allNodes := g.cache.ListNodes() if len(allNodes) == 0 { return nil, nil, nil, ErrNoNodesAvailable } // 过滤 predicates 算法执行失败的 node 作为抢占的候选 node potentialNodes := nodesWherePreemptionMightHelp(allNodes, fitError) // 如果过滤出的候选 node 为空则返回抢占者作为 nominatedPodsToClear if len(potentialNodes) == 0 { return nil, nil, []*v1.Pod{pod}, nil } // 获取 PodDisruptionBudget objects pdbs, err := g.pdbLister.List(labels.Everything()) if err != nil { return nil, nil, nil, err } // 过滤出可以抢占的 node 列表 nodeToVictims, err := g.selectNodesForPreemption(pluginContext, pod, g.nodeInfoSnapshot.NodeInfoMap, potentialNodes, g.predicates, g.predicateMetaProducer, g.schedulingQueue, pdbs) if err != nil { return nil, nil, nil, err } // 若有 extender 则执行 nodeToVictims, err = g.processPreemptionWithExtenders(pod, nodeToVictims) if err != nil { return nil, nil, nil, err } // 选出最佳的 node candidateNode := pickOneNodeForPreemption(nodeToVictims) if candidateNode == nil { return nil, nil, nil, nil } // 移除低优先级 pod 的 Nominated,更新这些 pod,移动到 activeQ 队列中,让调度器 // 为这些 pod 重新 bind node nominatedPods := g.getLowerPriorityNominatedPods(pod, candidateNode.Name) if nodeInfo, ok := g.nodeInfoSnapshot.NodeInfoMap[candidateNode.Name]; ok { return nodeInfo.Node(), nodeToVictims[candidateNode].Pods, nominatedPods, nil } return nil, nil, nil, fmt.Errorf( \"preemption failed: the target node %s has been deleted from scheduler cache\", candidateNode.Name) } 该函数中调用了多个函数: nodesWherePreemptionMightHelp():过滤 predicates 算法执行失败的 node selectNodesForPreemption():过滤出可以抢占的 node 列表 pickOneNodeForPreemption():选出最佳的 node getLowerPriorityNominatedPods():移除低优先级 pod 的 Nominated selectNodesForPreemption() 从 prediacates 算法执行失败的 node 列表中来寻找可以被抢占的 node,通过workqueue.ParallelizeUntil()并发执行checkNode()函数检查 node。 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:996 func (g *genericScheduler) selectNodesForPreemption( ...... ) (map[*v1.Node]*schedulerapi.Victims, error) { nodeToVictims := map[*v1.Node]*schedulerapi.Victims{} var resultLock sync.Mutex meta := metadataProducer(pod, nodeNameToInfo) // checkNode 函数 checkNode := func(i int) { nodeName := potentialNodes[i].Name var metaCopy predicates.PredicateMetadata if meta != nil { metaCopy = meta.ShallowCopy() } // 调用 selectVictimsOnNode 函数进行检查 pods, numPDBViolations, fits := g.selectVictimsOnNode(pluginContext, pod, metaCopy, nodeNameToInfo[nodeName], fitPredicates, queue, pdbs) if fits { resultLock.Lock() victims := schedulerapi.Victims{ Pods: pods, NumPDBViolations: numPDBViolations, } nodeToVictims[potentialNodes[i]] = &victims resultLock.Unlock() } } // 启动 16 个 goroutine 并发执行 workqueue.ParallelizeUntil(context.TODO(), 16, len(potentialNodes), checkNode) return nodeToVictims, nil } 其中调用的selectVictimsOnNode()是来获取每个 node 上 victims pod 的,首先移除所有低优先级的 pod 尝试抢占者是否可以调度成功,如果能够调度成功,然后基于 pod 是否有 PDB 被分为两组 violatingVictims 和 nonViolatingVictims,再对每一组的 pod 按优先级进行排序。PDB(pod 中断预算)是 kubernetes 保证副本高可用的一个对象。 然后开始逐一”删除“ pod 即要删掉最少的 pod 数来完成这次抢占即可,先从 violatingVictims(有PDB)的一组中进行”删除“ pod,并且记录删除有 PDB pod 的数量,然后再“删除” nonViolatingVictims 组中的 pod,每次”删除“一个 pod 都要检查一下抢占者是否能够运行在该 node 上即执行一次预选策略,若执行预选策略失败则该 node 当前不满足抢占需要继续”删除“ pod 并将该 pod 加入到 victims 中,直到”删除“足够多的 pod 可以满足抢占,最后返回 victims 以及删除有 PDB pod 的数量。 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:1086 func (g *genericScheduler) selectVictimsOnNode( ...... ) ([]*v1.Pod, int, bool) { if nodeInfo == nil { return nil, 0, false } potentialVictims := util.SortableList{CompFunc: util.MoreImportantPod} nodeInfoCopy := nodeInfo.Clone() removePod := func(rp *v1.Pod) { nodeInfoCopy.RemovePod(rp) if meta != nil { meta.RemovePod(rp, nodeInfoCopy.Node()) } } addPod := func(ap *v1.Pod) { nodeInfoCopy.AddPod(ap) if meta != nil { meta.AddPod(ap, nodeInfoCopy) } } // 先删除所有的低优先级 pod 检查是否能满足抢占 pod 的调度需求 podPriority := util.GetPodPriority(pod) for _, p := range nodeInfoCopy.Pods() { if util.GetPodPriority(p) pickOneNodeForPreemption() 用来选出最佳的 node 作为抢占者的 node,该函数主要基于 6 个原则: PDB violations 值最小的 node 挑选具有高优先级较少的 node 对每个 node 上所有 victims 的优先级进项累加,选取最小的 如果多个 node 优先级总和相等,选择具有最小 victims 数量的 node 如果多个 node 优先级总和相等,选择具有高优先级且 pod 运行时间最短的 如果依据以上策略仍然选出了多个 node 则直接返回第一个 node k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:867 func pickOneNodeForPreemption(nodesToVictims map[*v1.Node]*schedulerapi.Victims) *v1.Node { if len(nodesToVictims) == 0 { return nil } minNumPDBViolatingPods := math.MaxInt32 var minNodes1 []*v1.Node lenNodes1 := 0 for node, victims := range nodesToVictims { if len(victims.Pods) == 0 { // 若该 node 没有 victims 则返回 return node } numPDBViolatingPods := victims.NumPDBViolations if numPDBViolatingPods 以上就是对抢占机制代码的一个通读。 优先级与抢占机制的使用 1、创建 PriorityClass 对象: apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: \"This priority class should be used for XYZ service pods only.\" 2、在 deployment、statefulset 或者 pod 中声明使用已有的 priorityClass 对象即可 在 pod 中使用: apiVersion: v1 kind: Pod metadata: labels: app: nginx-a name: nginx-a spec: containers: - image: nginx:1.7.9 imagePullPolicy: IfNotPresent name: nginx-a ports: - containerPort: 80 protocol: TCP resources: requests: memory: \"64Mi\" cpu: 5 limits: memory: \"128Mi\" cpu: 5 priorityClassName: high-priority 在 deployment 中使用: template: spec: containers: - image: nginx name: nginx-deployment priorityClassName: high-priority 3、测试过程中可以看到高优先级的 nginx-a 会抢占 nginx-5754944d6c 的资源: $ kubectl get pod -o wide -w NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-5754944d6c-9mnxa 1/1 Running 0 37s 10.244.1.4 test-worker nginx-a 0/1 Pending 0 0s nginx-a 0/1 Pending 0 0s nginx-a 0/1 Pending 0 0s test-worker nginx-5754944d6c-9mnxa 1/1 Terminating 0 45s 10.244.1.4 test-worker nginx-5754944d6c-9mnxa 0/1 Terminating 0 46s 10.244.1.4 test-worker nginx-5754944d6c-9mnxa 0/1 Terminating 0 47s 10.244.1.4 test-worker nginx-5754944d6c-9mnxa 0/1 Terminating 0 47s 10.244.1.4 test-worker nginx-a 0/1 Pending 0 2s test-worker test-worker nginx-a 0/1 ContainerCreating 0 2s test-worker nginx-a 1/1 Running 0 4s 10.244.1.5 test-worker 总结 这篇文章主要讲述 kube-scheduler 中的优先级与抢占机制,可以看到抢占机制比 predicates 与 priorities 算法都要复杂,其中的许多细节仍然没有提到,本文只是通读了大部分代码,某些代码的实现需要精读,限于笔者时间的关系,对于 kube-scheduler 的代码暂时分享到此处。 参考: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/ Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/k8s_service_theory.html":{"url":"kubernetes/k8s_service_theory.html","title":"kubernetes service 原理解析","keywords":"","body":"为什么需要 service 在 kubernetes 中,当创建带有多个副本的 deployment 时,kubernetes 会创建出多个 pod,此时即一个服务后端有多个容器,那么在 kubernetes 中负载均衡怎么做,容器漂移后 ip 也会发生变化,如何做服务发现以及会话保持?这就是 service 的作用,service 是一组具有相同 label pod 集合的抽象,集群内外的各个服务可以通过 service 进行互相通信,当创建一个 service 对象时也会对应创建一个 endpoint 对象,endpoint 是用来做容器发现的,service 只是将多个 pod 进行关联,实际的路由转发都是由 kubernetes 中的 kube-proxy 组件来实现,因此,service 必须结合 kube-proxy 使用,kube-proxy 组件可以运行在 kubernetes 集群中的每一个节点上也可以只运行在单独的几个节点上,其会根据 service 和 endpoints 的变动来改变节点上 iptables 或者 ipvs 中保存的路由规则。 service 的工作原理 endpoints controller 是负责生成和维护所有 endpoints 对象的控制器,监听 service 和对应 pod 的变化,更新对应 service 的 endpoints 对象。当用户创建 service 后 endpoints controller 会监听 pod 的状态,当 pod 处于 running 且准备就绪时,endpoints controller 会将 pod ip 记录到 endpoints 对象中,因此,service 的容器发现是通过 endpoints 来实现的。而 kube-proxy 会监听 service 和 endpoints 的更新并调用其代理模块在主机上刷新路由转发规则。 service 的负载均衡 上文已经提到 service 实际的路由转发都是由 kube-proxy 组件来实现的,service 仅以一种 VIP(ClusterIP) 的形式存在,kube-proxy 主要实现了集群内部从 pod 到 service 和集群外部从 nodePort 到 service 的访问,kube-proxy 的路由转发规则是通过其后端的代理模块实现的,kube-proxy 的代理模块目前有四种实现方案,userspace、iptables、ipvs、kernelspace,其发展历程如下所示: kubernetes v1.0:services 仅是一个“4层”代理,代理模块只有 userspace kubernetes v1.1:Ingress API 出现,其代理“7层”服务,并且增加了 iptables 代理模块 kubernetes v1.2:iptables 成为默认代理模式 kubernetes v1.8:引入 ipvs 代理模块 kubernetes v1.9:ipvs 代理模块成为 beta 版本 kubernetes v1.11:ipvs 代理模式 GA 在每种模式下都有自己的负载均衡策略,下文会详解介绍。 userspace 模式 在 userspace 模式下,访问服务的请求到达节点后首先进入内核 iptables,然后回到用户空间,由 kube-proxy 转发到后端的 pod,这样流量从用户空间进出内核带来的性能损耗是不可接受的,所以也就有了 iptables 模式。 为什么 userspace 模式要建立 iptables 规则,因为 kube-proxy 监听的端口在用户空间,这个端口不是服务的访问端口也不是服务的 nodePort,因此需要一层 iptables 把访问服务的连接重定向给 kube-proxy 服务。 iptables 模式 iptables 模式是目前默认的代理方式,基于 netfilter 实现。当客户端请求 service 的 ClusterIP 时,根据 iptables 规则路由到各 pod 上,iptables 使用 DNAT 来完成转发,其采用了随机数实现负载均衡。 iptables 模式与 userspace 模式最大的区别在于,iptables 模块使用 DNAT 模块实现了 service 入口地址到 pod 实际地址的转换,免去了一次内核态到用户态的切换,另一个与 userspace 代理模式不同的是,如果 iptables 代理最初选择的那个 pod 没有响应,它不会自动重试其他 pod。 iptables 模式最主要的问题是在 service 数量大的时候会产生太多的 iptables 规则,使用非增量式更新会引入一定的时延,大规模情况下有明显的性能问题。 ipvs 模式 当集群规模比较大时,iptables 规则刷新会非常慢,难以支持大规模集群,因其底层路由表的实现是链表,对路由规则的增删改查都要涉及遍历一次链表,ipvs 的问世正是解决此问题的,ipvs 是 LVS 的负载均衡模块,与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能,几乎允许无限的规模扩张。 ipvs 支持三种负载均衡模式:DR模式(Direct Routing)、NAT 模式(Network Address Translation)、Tunneling(也称 ipip 模式)。三种模式中只有 NAT 支持端口映射,所以 ipvs 使用 NAT 模式。linux 内核原生的 ipvs 只支持 DNAT,当在数据包过滤,SNAT 和支持 NodePort 类型的服务这几个场景中ipvs 还是会使用 iptables。 此外,ipvs 也支持更多的负载均衡算法,例如: rr:round-robin/轮询 lc:least connection/最少连接 dh:destination hashing/目标哈希 sh:source hashing/源哈希 sed:shortest expected delay/预计延迟时间最短 nq:never queue/从不排队 userspace、iptables、ipvs 三种模式中默认的负载均衡策略都是通过 round-robin 算法来选择后端 pod 的,在 service 中可以通过设置 service.spec.sessionAffinity 的值实现基于客户端 ip 的会话亲和性,service.spec.sessionAffinity 的值默认为\"None\",可以设置为 \"ClientIP\",此外也可以使用 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 设置会话保持时间。kernelspace 主要是在 windows 下使用的,本文暂且不谈。 service 的类型 service 支持的类型也就是 kubernetes 中服务暴露的方式,默认有四种 ClusterIP、NodePort、LoadBalancer、ExternelName,此外还有 Ingress,下面会详细介绍每种类型 service 的具体使用场景。 ClusterIP ClusterIP 类型的 service 是 kubernetes 集群默认的服务暴露方式,它只能用于集群内部通信,可以被各 pod 访问,其访问方式为: pod ---> ClusterIP:ServicePort --> (iptables)DNAT --> PodIP:containePort ClusterIP Service 类型的结构如下图所示: NodePort 如果你想要在集群外访问集群内部的服务,可以使用这种类型的 service,NodePort 类型的 service 会在集群内部署了 kube-proxy 的节点打开一个指定的端口,之后所有的流量直接发送到这个端口,然后会被转发到 service 后端真实的服务进行访问。Nodeport 构建在 ClusterIP 上,其访问链路如下所示: client ---> NodeIP:NodePort ---> ClusterIP:ServicePort ---> (iptables)DNAT ---> PodIP:containePort 其对应具体的 iptables 规则会在后文进行讲解。 NodePort service 类型的结构如下图所示: LoadBalancer LoadBalancer 类型的 service 通常和云厂商的 LB 结合一起使用,用于将集群内部的服务暴露到外网,云厂商的 LoadBalancer 会给用户分配一个 IP,之后通过该 IP 的流量会转发到你的 service 上。 LoadBalancer service 类型的结构如下图所示: ExternelName 通过 CNAME 将 service 与 externalName 的值(比如:foo.bar.example.com)映射起来,这种方式用的比较少。 Ingress Ingress 其实不是 service 的一个类型,但是它可以作用于多个 service,被称为 service 的 service,作为集群内部服务的入口,Ingress 作用在七层,可以根据不同的 url,将请求转发到不同的 service 上。 Ingress 的结构如下图所示: service 的服务发现 虽然 service 的 endpoints 解决了容器发现问题,但不提前知道 service 的 Cluster IP,怎么发现 service 服务呢?service 当前支持两种类型的服务发现机制,一种是通过环境变量,另一种是通过 DNS。在这两种方案中,建议使用后者。 环境变量 当一个 pod 创建完成之后,kubelet 会在该 pod 中注册该集群已经创建的所有 service 相关的环境变量,但是需要注意的是,在 service 创建之前的所有 pod 是不会注册该环境变量的,所以在平时使用时,建议通过 DNS 的方式进行 service 之间的服务发现。 DNS 可以在集群中部署 CoreDNS 服务(旧版本的 kubernetes 群使用的是 kubeDNS), 来达到集群内部的 pod 通过DNS 的方式进行集群内部各个服务之间的通讯。 当前 kubernetes 集群默认使用 CoreDNS 作为默认的 DNS 服务,主要原因是 CoreDNS 是基于 Plugin 的方式进行扩展的,简单,灵活,并且不完全被Kubernetes所捆绑。 service 的使用 ClusterIP 方式 apiVersion: v1 kind: Service metadata: name: my-nginx spec: clusterIP: 10.105.146.177 ports: - port: 80 protocol: TCP targetPort: 8080 selector: app: my-nginx sessionAffinity: None type: ClusterIP NodePort 方式 apiVersion: v1 kind: Service metadata: name: my-nginx spec: ports: - nodePort: 30090 port: 80 protocol: TCP targetPort: 8080 selector: app: my-nginx sessionAffinity: None type: NodePort 其中 nodeport 字段表示通过 nodeport 方式访问的端口,port 表示通过 service 方式访问的端口,targetPort 表示 container port。 Headless service(就是没有 Cluster IP 的 service ) 当不需要负载均衡以及单独的 ClusterIP 时,可以通过指定 spec.clusterIP 的值为 None 来创建 Headless service,它会给一个集群内部的每个成员提供一个唯一的 DNS 域名来作为每个成员的网络标识,集群内部成员之间使用域名通信。 apiVersion: v1 kind: Service metadata: name: my-nginx spec: clusterIP: None ports: - nodePort: 30090 port: 80 protocol: TCP targetPort: 8080 selector: app: my-nginx 总结 本文主要讲了 kubernetes 中 service 的原理、实现以及使用方式,service 目前主要有 5 种服务暴露方式,service 的容器发现是通过 endpoints 来实现的,其服务发现主要是通过 DNS 实现的,其负载均衡以及流量转发是通过 kube-proxy 实现的。在后面的文章我会继续介绍 kube-proxy 的设计及实现。 参考: https://www.cnblogs.com/xzkzzz/p/9559362.html https://xigang.github.io/2019/07/21/kubernetes-service/ Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kube_proxy_process.html":{"url":"kubernetes/kube_proxy_process.html","title":"kube-proxy 源码分析","keywords":"","body":" 上篇文章 kubernetes service 原理解析 已经分析了 service 原理以 kube-proxy 中三种模式的原理,本篇文章会从源码角度分析 kube-proxy 的设计与实现。 kubernetes 版本: v1.16 kube-proxy 启动流程 前面的文章已经说过 kubernetes 中所有组件都是通过其 run() 方法启动主逻辑的,run() 方法调用之前会进行解析命令行参数、添加默认值等。下面就直接看 kube-proxy 的 run() 方法: 若启动时指定了 --write-config-to 参数,kube-proxy 只将启动的默认参数写到指定的配置文件中,然后退出 初始化 ProxyServer 对象 如果启动参数 --cleanup 设置为 true,则清理 iptables 和 ipvs 规则并退出 k8s.io/kubernetes/cmd/kube-proxy/app/server.go:290 func (o *Options) Run() error { defer close(o.errCh) // 1.如果指定了 --write-config-to 参数,则将默认的配置文件写到指定文件并退出 if len(o.WriteConfigTo) > 0 { return o.writeConfigFile() } // 2.初始化 ProxyServer 对象 proxyServer, err := NewProxyServer(o) if err != nil { return err } // 3.如果启动参数 --cleanup 设置为 true,则清理 iptables 和 ipvs 规则并退出 if o.CleanupAndExit { return proxyServer.CleanupAndExit() } o.proxyServer = proxyServer return o.runLoop() } Run() 方法中主要调用了 NewProxyServer() 方法来初始化 ProxyServer,然后会调用 runLoop() 启动主循环,继续看初始化 ProxyServer 的具体实现: 初始化 iptables、ipvs 相关的 interface 若启用了 ipvs 则检查内核版本、ipvs 依赖的内核模块、ipset 版本,内核模块主要包括:ip_vs,ip_vs_rr,ip_vs_wrr,ip_vs_sh,nf_conntrack_ipv4,nf_conntrack,若没有相关模块,kube-proxy 会尝试使用 modprobe 自动加载 根据 proxyMode 初始化 proxier,kube-proxy 启动后只运行一种 proxier k8s.io/kubernetes/cmd/kube-proxy/app/server_others.go:57 func NewProxyServer(o *Options) (*ProxyServer, error) { return newProxyServer(o.config, o.CleanupAndExit, o.master) } func newProxyServer( config *proxyconfigapi.KubeProxyConfiguration, cleanupAndExit bool, master string) (*ProxyServer, error) { ...... if c, err := configz.New(proxyconfigapi.GroupName); err == nil { c.Set(config) } else { return nil, fmt.Errorf(\"unable to register configz: %s\", err) } ...... // 1.关键依赖工具 iptables/ipvs/ipset/dbus var iptInterface utiliptables.Interface var ipvsInterface utilipvs.Interface var kernelHandler ipvs.KernelHandler var ipsetInterface utilipset.Interface var dbus utildbus.Interface // 2.执行 linux 命令行的工具 execer := exec.New() // 3.初始化 iptables/ipvs/ipset/dbus 对象 dbus = utildbus.New() iptInterface = utiliptables.New(execer, dbus, protocol) kernelHandler = ipvs.NewLinuxKernelHandler() ipsetInterface = utilipset.New(execer) // 4.检查该机器是否支持使用 ipvs 模式 canUseIPVS, _ := ipvs.CanUseIPVSProxier(kernelHandler, ipsetInterface) if canUseIPVS { ipvsInterface = utilipvs.New(execer) } if cleanupAndExit { return &ProxyServer{ ...... }, nil } // 5.初始化 kube client 和 event client client, eventClient, err := createClients(config.ClientConnection, master) if err != nil { return nil, err } ...... // 6.初始化 healthzServer var healthzServer *healthcheck.HealthzServer var healthzUpdater healthcheck.HealthzUpdater if len(config.HealthzBindAddress) > 0 { healthzServer = healthcheck.NewDefaultHealthzServer(config.HealthzBindAddress, 2*config.IPTables.SyncPeriod.Duration, recorder, nodeRef) healthzUpdater = healthzServer } // 7.proxier 是一个 interface,每种模式都是一个 proxier var proxier proxy.Provider // 8.根据 proxyMode 初始化 proxier proxyMode := getProxyMode(string(config.Mode), kernelHandler, ipsetInterface, iptables.LinuxKernelCompatTester{}) ...... if proxyMode == proxyModeIPTables { klog.V(0).Info(\"Using iptables Proxier.\") if config.IPTables.MasqueradeBit == nil { return nil, fmt.Errorf(\"unable to read IPTables MasqueradeBit from config\") } // 9.初始化 iptables 模式的 proxier proxier, err = iptables.NewProxier( ....... ) if err != nil { return nil, fmt.Errorf(\"unable to create proxier: %v\", err) } metrics.RegisterMetrics() } else if proxyMode == proxyModeIPVS { // 10.判断是够启用了 ipv6 双栈 if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) { ...... // 11.初始化 ipvs 模式的 proxier proxier, err = ipvs.NewDualStackProxier( ...... ) } else { proxier, err = ipvs.NewProxier( ...... ) } if err != nil { return nil, fmt.Errorf(\"unable to create proxier: %v\", err) } metrics.RegisterMetrics() } else { // 12.初始化 userspace 模式的 proxier proxier, err = userspace.NewProxier( ...... ) if err != nil { return nil, fmt.Errorf(\"unable to create proxier: %v\", err) } } iptInterface.AddReloadFunc(proxier.Sync) return &ProxyServer{ ...... }, nil } runLoop() 方法主要是启动 proxyServer。 k8s.io/kubernetes/cmd/kube-proxy/app/server.go:311 func (o *Options) runLoop() error { // 1.watch 配置文件变化 if o.watcher != nil { o.watcher.Run() } // 2.以 goroutine 方式启动 proxyServer go func() { err := o.proxyServer.Run() o.errCh o.proxyServer.Run() 中会启动已经初始化好的所有服务: 设定进程 OOMScore,可通过命令行配置,默认值为 --oom-score-adj=\"-999\" 启动 metric server 和 healthz server,两者分别监听 10256 和 10249 端口 设置内核参数 nf_conntrack_tcp_timeout_established 和 nf_conntrack_tcp_timeout_close_wait 将 proxier 注册到 serviceEventHandler、endpointsEventHandler 中 启动 informer 监听 service 和 endpoints 变化 执行 s.Proxier.SyncLoop(),启动 proxier 主循环 k8s.io/kubernetes/cmd/kube-proxy/app/server.go:527 func (s *ProxyServer) Run() error { ...... // 1.进程 OOMScore,避免进程因 oom 被杀掉,此处默认值为 -999 var oomAdjuster *oom.OOMAdjuster if s.OOMScoreAdj != nil { oomAdjuster = oom.NewOOMAdjuster() if err := oomAdjuster.ApplyOOMScoreAdj(0, int(*s.OOMScoreAdj)); err != nil { klog.V(2).Info(err) } } ...... // 2.启动 healthz server if s.HealthzServer != nil { s.HealthzServer.Run() } // 3.启动 metrics server if len(s.MetricsBindAddress) > 0 { ...... go wait.Until(func() { err := http.ListenAndServe(s.MetricsBindAddress, proxyMux) if err != nil { utilruntime.HandleError(fmt.Errorf(\"starting metrics server failed: %v\", err)) } }, 5*time.Second, wait.NeverStop) } // 4.配置 conntrack,设置内核参数 nf_conntrack_tcp_timeout_established 和 nf_conntrack_tcp_timeout_close_wait if s.Conntracker != nil { max, err := getConntrackMax(s.ConntrackConfiguration) if err != nil { return err } if max > 0 { err := s.Conntracker.SetMax(max) ...... } if s.ConntrackConfiguration.TCPEstablishedTimeout != nil && s.ConntrackConfiguration.TCPEstablishedTimeout.Duration > 0 { timeout := int(s.ConntrackConfiguration.TCPEstablishedTimeout.Duration / time.Second) if err := s.Conntracker.SetTCPEstablishedTimeout(timeout); err != nil { return err } } if s.ConntrackConfiguration.TCPCloseWaitTimeout != nil && s.ConntrackConfiguration.TCPCloseWaitTimeout.Duration > 0 { timeout := int(s.ConntrackConfiguration.TCPCloseWaitTimeout.Duration / time.Second) if err := s.Conntracker.SetTCPCloseWaitTimeout(timeout); err != nil { return err } } } ...... // 5.启动 informer 监听 Services 和 Endpoints 或者 EndpointSlices 信息 informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod, informers.WithTweakListOptions(func(options *metav1.ListOptions) { options.LabelSelector = labelSelector.String() })) // 6.将 proxier 注册到 serviceConfig、endpointsConfig 中 serviceConfig := config.NewServiceConfig(informerFactory.Core().V1().Services(), s.ConfigSyncPeriod) serviceConfig.RegisterEventHandler(s.Proxier) go serviceConfig.Run(wait.NeverStop) if utilfeature.DefaultFeatureGate.Enabled(features.EndpointSlice) { endpointSliceConfig := config.NewEndpointSliceConfig(informerFactory.Discovery().V1alpha1().EndpointSlices(), s.ConfigSyncPeriod) endpointSliceConfig.RegisterEventHandler(s.Proxier) go endpointSliceConfig.Run(wait.NeverStop) } else { endpointsConfig := config.NewEndpointsConfig(informerFactory.Core().V1().Endpoints(), s.ConfigSyncPeriod) endpointsConfig.RegisterEventHandler(s.Proxier) go endpointsConfig.Run(wait.NeverStop) } // 7.启动 informer informerFactory.Start(wait.NeverStop) s.birthCry() // 8.启动 proxier 主循环 s.Proxier.SyncLoop() return nil } 回顾一下整个启动逻辑: o.Run() --> o.runLoop() --> o.proxyServer.Run() --> s.Proxier.SyncLoop() o.Run() 中调用了 NewProxyServer() 来初始化 proxyServer 对象,其中包括初始化每种模式对应的 proxier,该方法最终会调用 s.Proxier.SyncLoop() 执行 proxier 的主循环。 proxier 的初始化 看完了启动流程的逻辑代码,接着再看一下各代理模式的初始化,上文已经提到每种模式都是一个 proxier,即要实现 proxy.Provider 对应的 interface,如下所示: type Provider interface { config.EndpointsHandler config.EndpointSliceHandler config.ServiceHandler Sync() SyncLoop() } 首先要实现 service、endpoints 和 endpointSlice 对应的 handler,也就是对 OnAdd、OnUpdate、OnDelete 、OnSynced 四种方法的处理,详细的代码在下文进行讲解。EndpointSlice 是在 v1.16 中新加入的一个 API。Sync() 和 SyncLoop() 是主要用来处理iptables 规则的方法。 iptables proxier 初始化 首先看 iptables 模式的 NewProxier()方法,其函数的具体执行逻辑为: 设置相关的内核参数route_localnet、bridge-nf-call-iptables 生成 masquerade 标记 设置默认调度算法 rr 初始化 proxier 对象 使用 BoundedFrequencyRunner 初始化 proxier.syncRunner,将 proxier.syncProxyRules 方法注入,BoundedFrequencyRunner 是一个管理器用于执行用户注入的函数,可以指定运行的时间策略。 k8s.io/kubernetes/pkg/proxy/iptables/proxier.go:249 func NewProxier(ipt utiliptables.Interface, ...... ) (*Proxier, error) { // 1.设置相关的内核参数 if val, _ := sysctl.GetSysctl(sysctlRouteLocalnet); val != 1 { ...... } if val, err := sysctl.GetSysctl(sysctlBridgeCallIPTables); err == nil && val != 1 { ...... } // 2.设置 masqueradeMark,默认为 0x00004000/0x00004000 // 用来标记 k8s 管理的报文,masqueradeBit 默认为 14 // 标记 0x4000 的报文(即 POD 发出的报文),在离开 Node 的时候需要进行 SNAT 转换 masqueradeValue := 1 ipvs proxier 初始化 ipvs NewProxier() 方法主要逻辑为: 设定内核参数,route_localnet、br_netfilter、bridge-nf-call-iptables、conntrack、conn_reuse_mode、ip_forward、arp_ignore、arp_announce 等 和 iptables 一样,对于 SNAT iptables 规则生成 masquerade 标记 设置默认调度算法 rr 初始化 proxier 对象 初始化 ipset 规则 初始化 syncRunner 将 proxier.syncProxyRules 方法注入 启动 gracefuldeleteManager 定时清理 RS (realServer) 记录 k8s.io/kubernetes/pkg/proxy/ipvs/proxier.go:316 func NewProxier(ipt utiliptables.Interface, ...... ) (*Proxier, error) { // 1.设定内核参数 if val, _ := sysctl.GetSysctl(sysctlRouteLocalnet); val != 1 { ...... } ...... // 2.生成 masquerade 标记 masqueradeValue := 1 userspace proxier 初始化 userspace NewProxier() 方法主要逻辑为: 初始化 iptables 规则 初始化 proxier 初始化 syncRunner 将 proxier.syncProxyRules 方法注入 k8s.io/kubernetes/pkg/proxy/userspace/proxier.go:187 func NewProxier(......) (*Proxier, error) { return NewCustomProxier(loadBalancer, listenIP, iptables, exec, pr, syncPeriod, minSyncPeriod, udpIdleTimeout, nodePortAddresses, newProxySocket) } func NewCustomProxier(......) (*Proxier, error) { ...... // 1.设置打开文件数 err = setRLimit(64 * 1000) if err != nil { return nil, fmt.Errorf(\"failed to set open file handler limit: %v\", err) } proxyPorts := newPortAllocator(pr) return createProxier(loadBalancer, listenIP, iptables, exec, hostIP, proxyPorts, syncPeriod, minSyncPeriod, udpIdleTimeout, makeProxySocket) } func createProxier(loadBalancer LoadBalancer, listenIP net.IP, iptables iptables.Interface, exec utilexec.Interface, hostIP net.IP, proxyPorts PortAllocator, syncPeriod, minSyncPeriod, udpIdleTimeout time.Duration, makeProxySocket ProxySocketFunc) (*Proxier, error) { if proxyPorts == nil { proxyPorts = newPortAllocator(utilnet.PortRange{}) } // 2.初始化 iptables 规则 if err := iptablesInit(iptables); err != nil { return nil, fmt.Errorf(\"failed to initialize iptables: %v\", err) } if err := iptablesFlush(iptables); err != nil { return nil, fmt.Errorf(\"failed to flush iptables: %v\", err) } // 3.初始化 proxier proxier := &Proxier{ ...... } // 4.初始化 syncRunner proxier.syncRunner = async.NewBoundedFrequencyRunner(\"userspace-proxy-sync-runner\", proxier.syncProxyRules, minSyncPeriod, syncPeriod, numBurstSyncs) return proxier, nil } proxier 接口实现 handler 的实现 上文已经提到过每种 proxier 都需要实现 interface 中的几个方法,首先看一下 ServiceHandler、EndpointsHandler 和 EndpointSliceHandler 相关的,对于 service、endpoints 和 endpointSlices 三种对象都实现了 OnAdd、OnUpdate、OnDelete 和 OnSynced 方法。 // 1.service 相关的方法 func (proxier *Proxier) OnServiceAdd(service *v1.Service) { proxier.OnServiceUpdate(nil, service) } func (proxier *Proxier) OnServiceUpdate(oldService, service *v1.Service) { if proxier.serviceChanges.Update(oldService, service) && proxier.isInitialized() { proxier.syncRunner.Run() } } func (proxier *Proxier) OnServiceDelete(service *v1.Service) { proxier.OnServiceUpdate(service, nil) } func (proxier *Proxier) OnServiceSynced(){ ...... proxier.syncProxyRules() } // 2.endpoints 相关的方法 func (proxier *Proxier) OnEndpointsAdd(endpoints *v1.Endpoints) { proxier.OnEndpointsUpdate(nil, endpoints) } func (proxier *Proxier) OnEndpointsUpdate(oldEndpoints, endpoints *v1.Endpoints) { if proxier.endpointsChanges.Update(oldEndpoints, endpoints) && proxier.isInitialized() { proxier.Sync() } } func (proxier *Proxier) OnEndpointsDelete(endpoints *v1.Endpoints) { proxier.OnEndpointsUpdate(endpoints, nil) } func (proxier *Proxier) OnEndpointsSynced() { ...... proxier.syncProxyRules() } // 3.endpointSlice 相关的方法 func (proxier *Proxier) OnEndpointSliceAdd(endpointSlice *discovery.EndpointSlice) { if proxier.endpointsChanges.EndpointSliceUpdate(endpointSlice, false) && proxier.isInitialized() { proxier.Sync() } } func (proxier *Proxier) OnEndpointSliceUpdate(_, endpointSlice *discovery.EndpointSlice) { if proxier.endpointsChanges.EndpointSliceUpdate(endpointSlice, false) && proxier.isInitialized() { proxier.Sync() } } func (proxier *Proxier) OnEndpointSliceDelete(endpointSlice *discovery.EndpointSlice) { if proxier.endpointsChanges.EndpointSliceUpdate(endpointSlice, true) && proxier.isInitialized() { proxier.Sync() } } func (proxier *Proxier) OnEndpointSlicesSynced() { ...... proxier.syncProxyRules() } 在启动逻辑的 Run() 方法中 proxier 已经被注册到了 serviceConfig、endpointsConfig、endpointSliceConfig 中,当启动 informer,cache 同步完成后会调用 OnSynced() 方法,之后当 watch 到变化后会调用 proxier 中对应的 OnUpdate() 方法进行处理,OnSynced() 会直接调用 proxier.syncProxyRules() 来刷新iptables 规则,而 OnUpdate() 会调用 proxier.syncRunner.Run() 方法,其最终也是调用 proxier.syncProxyRules() 方法刷新规则的,这种转换是在 BoundedFrequencyRunner 中体现出来的,下面看一下具体实现。 Sync() 以及 SyncLoop() 的实现 每种 proxier 的 Sync() 以及 SyncLoop() 方法如下所示,都是调用 syncRunner 中的相关方法,而 syncRunner 在前面的 NewProxier() 中已经说过了,syncRunner 是调用 async.NewBoundedFrequencyRunner() 方法初始化,至此,基本上可以确定了所有的核心都是在 BoundedFrequencyRunner 中实现的。 func NewProxier() (*Proxier, error) { ...... proxier.syncRunner = async.NewBoundedFrequencyRunner(\"sync-runner\", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs) ...... } // Sync() func (proxier *Proxier) Sync() { proxier.syncRunner.Run() } // SyncLoop() func (proxier *Proxier) SyncLoop() { if proxier.healthzServer != nil { proxier.healthzServer.UpdateTimestamp() } proxier.syncRunner.Loop(wait.NeverStop) } NewBoundedFrequencyRunner()是其初始化的函数,其中的参数 minInterval和 maxInterval 分别对应 proxier 中的 minSyncPeriod 和 syncPeriod,两者的默认值分别为 0s 和 30s,其值可以使用 --iptables-min-sync-period 和 --iptables-sync-period 启动参数来指定。 k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:134 func NewBoundedFrequencyRunner(name string, fn func(), minInterval, maxInterval time.Duration, burstRuns int) *BoundedFrequencyRunner { timer := realTimer{Timer: time.NewTimer(0)} // 执行定时器 = minInterval (%v)\", name, maxInterval, minInterval)) } if timer == nil { panic(fmt.Sprintf(\"%s: timer must be non-nil\", name)) } bfr := &BoundedFrequencyRunner{ name: name, fn: fn, // 被调用的函数,proxier.syncProxyRules minInterval: minInterval, maxInterval: maxInterval, run: make(chan struct{}, 1), timer: timer, } // 由于默认的 minInterval = 0,此处使用 nullLimiter if minInterval == 0 { bfr.limiter = nullLimiter{} } else { // 采用“令牌桶”算法实现流控机制 qps := float32(time.Second) / float32(minInterval) bfr.limiter = flowcontrol.NewTokenBucketRateLimiterWithClock(qps, burstRuns, timer) } return bfr } 在启动流程 Run() 方法最后调用的 s.Proxier.SyncLoop() 最终调用的是 BoundedFrequencyRunner 的 Loop()方法,如下所示: k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:169 func (bfr *BoundedFrequencyRunner) Loop(stop proxier 的 OnUpdate() 中调用的 syncRunner.Run() 其实只是在 bfr.run 这个带 buffer 的 channel 中发送了一条数据,在 BoundedFrequencyRunner 的 Loop()方法中接收到该数据后会调用 bfr.tryRun() 进行处理: k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:191 func (bfr *BoundedFrequencyRunner) Run() { select { case bfr.run 而 tryRun() 方法才是最终调用 syncProxyRules() 刷新iptables 规则的。 k8s.io/kubernetes/pkg/util/async/bounded_frequency_runner.go:211 func (bfr *BoundedFrequencyRunner) tryRun() { bfr.mu.Lock() defer bfr.mu.Unlock() if bfr.limiter.TryAccept() { // 执行 fn() 即 syncProxyRules() 刷新iptables 规则 bfr.fn() bfr.lastRun = bfr.timer.Now() bfr.timer.Stop() bfr.timer.Reset(bfr.maxInterval) return } elapsed := bfr.timer.Since(bfr.lastRun) // how long since last run nextPossible := bfr.minInterval - elapsed // time to next possible run nextScheduled := bfr.maxInterval - elapsed // time to next periodic run if nextPossible 通过以上分析可知,syncProxyRules() 是每个 proxier 的核心方法,启动 informer cache 同步完成后会直接调用 proxier.syncProxyRules() 刷新iptables 规则,之后如果 informer watch 到相关对象的变化后会调用 BoundedFrequencyRunner 的 tryRun()来刷新iptables 规则,定时器每 30s 会执行一次iptables 规则的刷新。 总结 本文主要介绍了 kube-proxy 的启动逻辑以及三种模式 proxier 的初始化,还有最终调用刷新iptables 规则的 BoundedFrequencyRunner,可以看到其中的代码写的很巧妙。而每种模式下的iptables 规则是如何创建、刷新以及转发的是如何实现的会在后面的文章中进行分析。 Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kube_proxy_iptables.html":{"url":"kubernetes/kube_proxy_iptables.html","title":"kube-proxy iptables 模式源码分析","keywords":"","body":"iptables 的功能 在前面的文章中已经介绍过 iptable 的一些基本信息,本文会深入介绍 kube-proxy iptables 模式下的工作原理,本文中多处会与 iptables 的知识相关联,若没有 iptables 基础,请先自行补充。 iptables 的功能: 流量转发:DNAT 实现 IP 地址和端口的映射; 负载均衡:statistic 模块为每个后端设置权重; 会话保持:recent 模块设置会话保持时间; iptables 有五张表和五条链,五条链分别对应为: PREROUTING 链:数据包进入路由之前,可以在此处进行 DNAT; INPUT 链:一般处理本地进程的数据包,目的地址为本机; FORWARD 链:一般处理转发到其他机器或者 network namespace 的数据包; OUTPUT 链:原地址为本机,向外发送,一般处理本地进程的输出数据包; POSTROUTING 链:发送到网卡之前,可以在此处进行 SNAT; 五张表分别为: filter 表:用于控制到达某条链上的数据包是继续放行、直接丢弃(drop)还是拒绝(reject); nat 表:network address translation 网络地址转换,用于修改数据包的源地址和目的地址; mangle 表:用于修改数据包的 IP 头信息; raw 表:iptables 是有状态的,其对数据包有链接追踪机制,连接追踪信息在 /proc/net/nf_conntrack 中可以看到记录,而 raw 是用来去除链接追踪机制的; security 表:最不常用的表,用在 SELinux 上; 这五张表是对 iptables 所有规则的逻辑集群且是有顺序的,当数据包到达某一条链时会按表的顺序进行处理,表的优先级为:raw、mangle、nat、filter、security。 iptables 的工作流程如下图所示: kube-proxy 的 iptables 模式 kube-proxy 组件负责维护 node 节点上的防火墙规则和路由规则,在 iptables 模式下,会根据 service 以及 endpoints 对象的改变来实时刷新规则,kube-proxy 使用了 iptables 的 filter 表和 nat 表,并对 iptables 的链进行了扩充,自定义了 KUBE-SERVICES、KUBE-EXTERNAL-SERVICES、KUBE-NODEPORTS、KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-MARK-DROP、KUBE-FORWARD 七条链,另外还新增了以“KUBE-SVC-xxx”和“KUBE-SEP-xxx”开头的数个链,除了创建自定义的链以外还将自定义链插入到已有链的后面以便劫持数据包。 在 nat 表中自定义的链以及追加的链如下所示: 在 filter 表定义的链以及追加的链如下所示如下所示: 对于 KUBE-MARK-MASQ 链中所有规则设置了 kubernetes 独有的 MARK 标记,在 KUBE-POSTROUTING 链中对 node 节点上匹配 kubernetes 独有 MARK 标记的数据包,进行 SNAT 处理。 -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 Kube-proxy 接着为每个服务创建 KUBE-SVC-xxx 链,并在 nat 表中将 KUBE-SERVICES 链中每个目标地址是service 的数据包导入这个 KUBE-SVC-xxx 链,如果 endpoint 尚未创建,则 KUBE-SVC-xxx 链中没有规则,任何 incomming packets 在规则匹配失败后会被 KUBE-MARK-DROP 进行标记然后再 FORWARD 链中丢弃。 这些自定义链与 iptables 的表结合后如下所示,笔者只画出了 PREROUTING 和 OUTPUT 链中追加的链以及部分自定义链,因为 PREROUTING 和 OUTPUT 的首条 NAT 规则都先将所有流量导入KUBE-SERVICE 链中,这样就截获了所有的入流量和出流量,进而可以对 k8s 相关流量进行重定向处理。 kubernetes 自定义链中数据包的详细流转可以参考: iptables 规则分析 clusterIP 访问方式 创建一个 clusterIP 访问方式的 service 以及带有两个副本,从 pod 中访问 clusterIP 的 iptables 规则流向为: PREROUTING --> KUBE-SERVICE --> KUBE-SVC-XXX --> KUBE-SEP-XXX 访问流程如下所示: 1、对于进入 PREROUTING 链的都转到 KUBE-SERVICES 链进行处理; 2、在 KUBE-SERVICES 链,对于访问 clusterIP 为 10.110.243.155 的转发到 KUBE-SVC-5SB6FTEHND4GTL2W; 3、访问 KUBE-SVC-5SB6FTEHND4GTL2W 的使用随机数负载均衡,并转发到 KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-SEP-OVNLTDWFHTHII4SC 上; 4、KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-SEP-OVNLTDWFHTHII4SC 对应 endpoint 中的 pod 192.168.137.147 和 192.168.98.213,设置 mark 标记,进行 DNAT 并转发到具体的 pod 上,如果某个 service 的 endpoints 中没有 pod,那么针对此 service 的请求将会被 drop 掉; // 1. -A PREROUTING -m comment --comment \"kubernetes service portals\" -j KUBE-SERVICES // 2. -A KUBE-SERVICES -d 10.110.243.155/32 -p tcp -m comment --comment \"pks-system/tenant-service: cluster IP\" -m tcp --dport 7000 -j KUBE-SVC-5SB6FTEHND4GTL2W // 3. -A KUBE-SVC-5SB6FTEHND4GTL2W -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-CI5ZO3FTK7KBNRMG -A KUBE-SVC-5SB6FTEHND4GTL2W -j KUBE-SEP-OVNLTDWFHTHII4SC // 4. -A KUBE-SEP-CI5ZO3FTK7KBNRMG -s 192.168.137.147/32 -j KUBE-MARK-MASQ -A KUBE-SEP-CI5ZO3FTK7KBNRMG -p tcp -m tcp -j DNAT --to-destination 192.168.137.147:7000 -A KUBE-SEP-OVNLTDWFHTHII4SC -s 192.168.98.213/32 -j KUBE-MARK-MASQ -A KUBE-SEP-OVNLTDWFHTHII4SC -p tcp -m tcp -j DNAT --to-destination 192.168.98.213:7000 nodePort 方式 在 nodePort 方式下,会用到 KUBE-NODEPORTS 规则链,通过 iptables -t nat -L -n 可以看到 KUBE-NODEPORTS 位于 KUBE-SERVICE 链的最后一个,iptables 在处理报文时会优先处理目的 IP 为clusterIP 的报文,在前面的 KUBE-SVC-XXX 都匹配失败之后再去使用 nodePort 方式进行匹配。 创建一个 nodePort 访问方式的 service 以及带有两个副本,访问 nodeport 的 iptables 规则流向为: 1、非本机访问 PREROUTING --> KUBE-SERVICE --> KUBE-NODEPORTS --> KUBE-SVC-XXX --> KUBE-SEP-XXX 2、本机访问 OUTPUT --> KUBE-SERVICE --> KUBE-NODEPORTS --> KUBE-SVC-XXX --> KUBE-SEP-XXX 该服务的 nodePort 端口为 30070,其 iptables 访问规则和使用 clusterIP 方式访问有点类似,不过 nodePort 方式会比 clusterIP 的方式多走一条链 KUBE-NODEPORTS,其会在 KUBE-NODEPORTS 链设置 mark 标记并转发到 KUBE-SVC-5SB6FTEHND4GTL2W,nodeport 与 clusterIP 访问方式最后都是转发到了 KUBE-SVC-xxx 链。 1、经过 PREROUTING 转到 KUBE-SERVICES 2、经过 KUBE-SERVICES 转到 KUBE-NODEPORTS 3、经过 KUBE-NODEPORTS 转到 KUBE-SVC-5SB6FTEHND4GTL2W 4、经过 KUBE-SVC-5SB6FTEHND4GTL2W 转到 KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-SEP-VR562QDKF524UNPV 5、经过 KUBE-SEP-CI5ZO3FTK7KBNRMG 和 KUBE-SEP-VR562QDKF524UNPV 分别转到 192.168.137.147:7000 和 192.168.89.11:7000 // 1. -A PREROUTING -m comment --comment \"kubernetes service portals\" -j KUBE-SERVICES // 2. ...... -A KUBE-SERVICES xxx ...... -A KUBE-SERVICES -m comment --comment \"kubernetes service nodeports; NOTE: this must be the last rule in this chain\" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS // 3. -A KUBE-NODEPORTS -p tcp -m comment --comment \"pks-system/tenant-service:\" -m tcp --dport 30070 -j KUBE-MARK-MASQ -A KUBE-NODEPORTS -p tcp -m comment --comment \"pks-system/tenant-service:\" -m tcp --dport 30070 -j KUBE-SVC-5SB6FTEHND4GTL2W // 4、 -A KUBE-SVC-5SB6FTEHND4GTL2W -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-CI5ZO3FTK7KBNRMG -A KUBE-SVC-5SB6FTEHND4GTL2W -j KUBE-SEP-VR562QDKF524UNPV // 5、 -A KUBE-SEP-CI5ZO3FTK7KBNRMG -s 192.168.137.147/32 -j KUBE-MARK-MASQ -A KUBE-SEP-CI5ZO3FTK7KBNRMG -p tcp -m tcp -j DNAT --to-destination 192.168.137.147:7000 -A KUBE-SEP-VR562QDKF524UNPV -s 192.168.89.11/32 -j KUBE-MARK-MASQ -A KUBE-SEP-VR562QDKF524UNPV -p tcp -m tcp -j DNAT --to-destination 192.168.89.11:7000 其他访问方式对应的 iptables 规则可自行分析。 iptables 模式源码分析 kubernetes 版本:v1.16 上篇文章已经在源码方面做了许多铺垫,下面就直接看 kube-proxy iptables 模式的核心方法。首先回顾一下 iptables 模式的调用流程,kube-proxy 根据给定的 proxyMode 初始化对应的 proxier 后会调用 Proxier.SyncLoop() 执行 proxier 的主循环,而其最终会调用 proxier.syncProxyRules() 刷新 iptables 规则。 proxier.SyncLoop() --> proxier.syncRunner.Loop()-->bfr.tryRun()-->bfr.fn()-->proxier.syncProxyRules() proxier.syncProxyRules()这个函数比较长,大约 800 行,其中有许多冗余的代码,代码可读性不佳,我们只需理解其基本流程即可,该函数的主要功能为: 更新proxier.endpointsMap,proxier.servieMap 创建自定义链 将当前内核中 filter 表和 nat 表中的全部规则导入到内存中 为每个 service 创建规则 为 clusterIP 设置访问规则 为 externalIP 设置访问规则 为 ingress 设置访问规则 为 nodePort 设置访问规则 为 endpoint 生成规则链 写入 DNAT 规则 删除不再使用的服务自定义链 使用 iptables-restore 同步规则 首先是更新 proxier.endpointsMap,proxier.servieMap 两个对象。 k8s.io/kubernetes/pkg/proxy/iptables/proxier.go:677 func (proxier *Proxier) syncProxyRules() { ...... serviceUpdateResult := proxy.UpdateServiceMap(proxier.serviceMap, proxier.serviceChanges) endpointUpdateResult := proxier.endpointsMap.Update(proxier.endpointsChanges) staleServices := serviceUpdateResult.UDPStaleClusterIP for _, svcPortName := range endpointUpdateResult.StaleServiceNames { if svcInfo, ok := proxier.serviceMap[svcPortName]; ok && svcInfo != nil && svcInfo.Protocol() == v1.ProtocolUDP { staleServices.Insert(svcInfo.ClusterIP().String()) for _, extIP := range svcInfo.ExternalIPStrings() { staleServices.Insert(extIP) } } } ...... 然后创建所需要的 iptable 链: for _, jump := range iptablesJumpChains { // 创建自定义链 if _, err := proxier.iptables.EnsureChain(jump.table, jump.dstChain); err != nil { ..... } args := append(jump.extraArgs, ...... ) //插入到已有的链 if _, err := proxier.iptables.EnsureRule(utiliptables.Prepend, jump.table, jump.srcChain, args...); err != nil { ...... } } 将当前内核中 filter 表和 nat 表中的全部规则临时导出到 buffer 中: err := proxier.iptables.SaveInto(utiliptables.TableFilter, proxier.existingFilterChainsData) if err != nil { } else { existingFilterChains = utiliptables.GetChainLines(utiliptables.TableFilter, proxier.existingFilterChainsData.Bytes()) } ...... err = proxier.iptables.SaveInto(utiliptables.TableNAT, proxier.iptablesData) if err != nil { } else { existingNATChains = utiliptables.GetChainLines(utiliptables.TableNAT, proxier.iptablesData.Bytes()) } writeLine(proxier.filterChains, \"*filter\") writeLine(proxier.natChains, \"*nat\") 检查已经创建出的表是否存在: for _, chainName := range []utiliptables.Chain{kubeServicesChain, kubeExternalServicesChain, kubeForwardChain} { if chain, ok := existingFilterChains[chainName]; ok { writeBytesLine(proxier.filterChains, chain) } else { writeLine(proxier.filterChains, utiliptables.MakeChainLine(chainName)) } } for _, chainName := range []utiliptables.Chain{kubeServicesChain, kubeNodePortsChain, kubePostroutingChain, KubeMarkMasqChain} { if chain, ok := existingNATChains[chainName]; ok { writeBytesLine(proxier.natChains, chain) } else { writeLine(proxier.natChains, utiliptables.MakeChainLine(chainName)) } } 写入 SNAT 地址伪装规则,在 POSTROUTING 阶段对地址进行 MASQUERADE 处理,原始请求源 IP 将被丢失,被请求 pod 的应用看到为 NodeIP 或 CNI 设备 IP(bridge/vxlan设备): masqRule := []string{ ...... } if proxier.iptables.HasRandomFully() { masqRule = append(masqRule, \"--random-fully\") } else { } writeLine(proxier.natRules, masqRule...) writeLine(proxier.natRules, []string{ ...... }...) 为每个 service 创建规则,创建 KUBE-SVC-xxx 和 KUBE-XLB-xxx 链、创建 service portal 规则、为 clusterIP 创建规则: for svcName, svc := range proxier.serviceMap { svcInfo, ok := svc.(*serviceInfo) ...... if hasEndpoints { ...... } svcXlbChain := svcInfo.serviceLBChainName if svcInfo.OnlyNodeLocalEndpoints() { ...... } if hasEndpoints { ...... } else { ...... } 若服务使用了 externalIP,创建对应的规则: for _, externalIP := range svcInfo.ExternalIPStrings() { if local, err := utilproxy.IsLocalIP(externalIP); err != nil { ...... if proxier.portsMap[lp] != nil { ...... } else { ...... } } if hasEndpoints { ...... } else { ...... } } 若服务使用了 ingress,创建对应的规则: for _, ingress := range svcInfo.LoadBalancerIPStrings() { if ingress != \"\" { if hasEndpoints { ...... if !svcInfo.OnlyNodeLocalEndpoints() { ...... } if len(svcInfo.LoadBalancerSourceRanges()) == 0 { ...... } else { ...... } ...... } else { ...... } } } 若使用了 nodePort,创建对应的规则: if svcInfo.NodePort() != 0 { addresses, err := utilproxy.GetNodeAddresses(proxier.nodePortAddresses, proxier.networkInterfacer) lps := make([]utilproxy.LocalPort, 0) for address := range addresses { ...... lps = append(lps, lp) } for _, lp := range lps { if proxier.portsMap[lp] != nil { } else if svcInfo.Protocol() != v1.ProtocolSCTP { socket, err := proxier.portMapper.OpenLocalPort(&lp) ...... if lp.Protocol == \"udp\" { ...... } replacementPortsMap[lp] = socket } } if hasEndpoints { ...... } else { ...... } } 为 endpoint 生成规则链 KUBE-SEP-XXX: endpoints = endpoints[:0] endpointChains = endpointChains[:0] var endpointChain utiliptables.Chain for _, ep := range proxier.endpointsMap[svcName] { epInfo, ok := ep.(*endpointsInfo) ...... if chain, ok := existingNATChains[utiliptables.Chain(endpointChain)]; ok { writeBytesLine(proxier.natChains, chain) } else { writeLine(proxier.natChains, utiliptables.MakeChainLine(endpointChain)) } activeNATChains[endpointChain] = true } 如果创建 service 时指定了 SessionAffinity 为 clientIP 则使用 recent 创建保持会话连接的规则: if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { for _, endpointChain := range endpointChains { ...... } } 写入负载均衡和 DNAT 规则,对于 endpoints 中的 pod 使用随机访问负载均衡策略。 在 iptables 规则中加入该 service 对应的自定义链“KUBE-SVC-xxx”,如果该服务对应的 endpoints 大于等于2,则添加负载均衡规则; 针对非本地 Node 上的 pod,需进行 DNAT,将请求的目标地址设置成候选的 pod 的 IP 后进行路由,KUBE-MARK-MASQ 将重设(伪装)源地址; for i, endpointChain := range endpointChains { ...... if svcInfo.OnlyNodeLocalEndpoints() && endpoints[i].IsLocal { ...... } ...... epIP := endpoints[i].IP() if epIP == \"\" { ...... } ...... args = append(args, \"-j\", string(endpointChain)) writeLine(proxier.natRules, args...) ...... if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { ...... } ...... writeLine(proxier.natRules, args...) } 若启用了 clusterCIDR 则生成对应的规则链: if len(proxier.clusterCIDR) > 0 { ...... writeLine(proxier.natRules, args...) } 为本机的 pod 开启会话保持: args = append(args[:0], \"-A\", string(svcXlbChain)) writeLine(proxier.natRules, ......) numLocalEndpoints := len(localEndpointChains) if numLocalEndpoints == 0 { ...... writeLine(proxier.natRules, args...) } else { if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { for _, endpointChain := range localEndpointChains { ...... } } ...... for i, endpointChain := range localEndpointChains { ...... args = append(args, \"-j\", string(endpointChain)) writeLine(proxier.natRules, args...) } } } 删除不存在服务的自定义链,KUBE-SVC-xxx、KUBE-SEP-xxx、KUBE-FW-xxx、KUBE-XLB-xxx: for chain := range existingNATChains { if !activeNATChains[chain] { ...... if !strings.HasPrefix(chainString, \"KUBE-SVC-\") && !strings.HasPrefix(chainString, \"KUBE-SEP-\") && !strings.HasPrefix(chainString, \"KUBE-FW-\") && ! strings.HasPrefix(chainString, \"KUBE-XLB-\") { ...... continue } writeBytesLine(proxier.natChains, existingNATChains[chain]) writeLine(proxier.natRules, \"-X\", chainString) } } 在 KUBE-SERVICES 链最后添加 nodePort 规则: addresses, err := utilproxy.GetNodeAddresses(proxier.nodePortAddresses, proxier.networkInterfacer) if err != nil { ...... } else { for address := range addresses { if utilproxy.IsZeroCIDR(address) { ...... } if isIPv6 && !utilnet.IsIPv6String(address) || !isIPv6 && utilnet.IsIPv6String(address) { ...... } ..... writeLine(proxier.natRules, args...) } } 为 INVALID 状态的包添加规则,为 KUBE-FORWARD 链添加对应的规则: writeLine(proxier.filterRules, ...... ) writeLine(proxier.filterRules, ...... ) if len(proxier.clusterCIDR) != 0 { writeLine(proxier.filterRules, ...... ) writeLine(proxier.filterRules, ...... ) } 在结尾添加标志: writeLine(proxier.filterRules, \"COMMIT\") writeLine(proxier.natRules, \"COMMIT\") 使用 iptables-restore 同步规则: proxier.iptablesData.Reset() proxier.iptablesData.Write(proxier.filterChains.Bytes()) proxier.iptablesData.Write(proxier.filterRules.Bytes()) proxier.iptablesData.Write(proxier.natChains.Bytes()) proxier.iptablesData.Write(proxier.natRules.Bytes()) err = proxier.iptables.RestoreAll(proxier.iptablesData.Bytes(), utiliptables.NoFlushTables, utiliptables.RestoreCounters) if err != nil { ...... } 以上就是对 kube-proxy iptables 代理模式核心源码的一个走读。 总结 本文主要讲了 kube-proxy iptables 模式的实现,可以看到其中的 iptables 规则是相当复杂的,在实际环境中尽量根据已有服务再来梳理整个 iptables 规则链就比较清楚了,笔者对于 iptables 的知识也是现学的,文中如有不当之处望指正。上面分析完了整个 iptables 模式的功能,但是 iptable 存在一些性能问题,比如有规则线性匹配时延、规则更新时延、可扩展性差等,为了解决这些问题于是有了 ipvs 模式,在下篇文章中会继续介绍 ipvs 模式的实现。 参考: https://www.jianshu.com/p/a978af8e5dd8 https://blog.csdn.net/ebay/article/details/52798074 https://blog.csdn.net/horsefoot/article/details/51249161 https://rootdeep.github.io/posts/kube-proxy-code-analysis/ https://www.cnblogs.com/charlieroro/p/9588019.html Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kube_proxy_ipvs.html":{"url":"kubernetes/kube_proxy_ipvs.html","title":"kube-proxy ipvs 模式源码分析","keywords":"","body":"前几篇文章已经分析了 service 的原理以及 kube-proxy iptables 模式的原理与实现,本篇文章会继续分析 kube-proxy ipvs 模式的原理与实现。 ipvs ipvs (IP Virtual Server) 是基于 Netfilter 的,作为 linux 内核的一部分实现了传输层负载均衡,ipvs 集成在LVS(Linux Virtual Server)中,它在主机中运行,并在真实服务器集群前充当负载均衡器。ipvs 可以将对 TCP/UDP 服务的请求转发给后端的真实服务器,因此 ipvs 天然支持 Kubernetes Service。ipvs 也包含了多种不同的负载均衡算法,例如轮询、最短期望延迟、最少连接以及各种哈希方法等,ipvs 的设计就是用来为大规模服务进行负载均衡的。 ipvs 的负载均衡方式 ipvs 有三种负载均衡方式,分别为: NAT TUN DR 关于三种模式的原理可以参考:LVS 配置小结。 上面的负载均衡方式中只有 NAT 模式可以进行端口映射,因此 kubernetes 中 ipvs 的实现使用了 NAT 模式,用来将 service IP 和 service port 映射到后端的 container ip 和container port。 NAT 模式下的工作流程如下所示: +--------+ | Client | +--------+ (CIP) 其具体流程为:当用户发起一个请求时,请求从 VIP 接口流入,此时数据源地址是 CIP,目标地址是 VIP,当接收到请求后拆掉 mac 地址封装后看到目标 IP 地址就是自己,按照正常流程会通过 INPUT 转入用户空间,但此时工作在 INPUT 链上的 ipvs 会强行将数据转到 POSTROUTING 链上,并根据相应的负载均衡算法选择后端具体的服务器,再通过 DNAT 转发给 Real server,此时源地址 CIP,目标地址变成了 RIP。 ipvs 与 iptables 的区别与联系 区别: 底层数据结构:iptables 使用链表,ipvs 使用哈希表 负载均衡算法:iptables 只支持随机、轮询两种负载均衡算法而 ipvs 支持的多达 8 种; 操作工具:iptables 需要使用 iptables 命令行工作来定义规则,ipvs 需要使用 ipvsadm 来定义规则。 此外 ipvs 还支持 realserver 运行状况检查、连接重试、端口映射、会话保持等功能。 联系: ipvs 和 iptables 都是基于 netfilter内核模块,两者都是在内核中的五个钩子函数处工作,下图是 ipvs 所工作的几个钩子函数: 关于 kube-proxy iptables 与 ipvs 模式的区别,更多详细信息可以查看官方文档:https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/README.md。 ipset IP sets 是 Linux 内核中的一个框架,可以由 ipset 命令进行管理。根据不同的类型,IP set 可以以某种方式保存 IP地址、网络、(TCP/UDP)端口号、MAC地址、接口名或它们的组合,并且能够快速匹配。 根据官网的介绍,若有以下使用场景: 在保存了多个 IP 地址或端口号的 iptables 规则集合中想使用哈希查找; 根据 IP 地址或端口动态更新 iptables 规则时希望在性能上无损; 在使用 iptables 工具创建一个基于 IP 地址和端口的复杂规则时觉得非常繁琐; 此时,使用 ipset 工具可能是你最好的选择。 ipset 是 iptables 的一种扩展,在 iptables 中可以使用-m set启用 ipset 模块,具体来说,ipvs 使用 ipset 来存储需要 NAT 或 masquared 时的 ip 和端口列表。在数据包过滤过程中,首先遍历 iptables 规则,在定义了使用 ipset 的条件下会跳转到 ipset 列表中进行匹配。 kube-proxy ipvs 模式 kube-proxy 的 ipvs 模式是在 2015 年由 k8s 社区的大佬 thockin 提出的(Try kube-proxy via ipvs instead of iptables or userspace),在 2017 年由华为云团队实现的(Implement IPVS-based in-cluster service load balancing)。前面的文章已经提到了,在kubernetes v1.8 中已经引入了 ipvs 模式。 kube-proxy 在 ipvs 模式下自定义了八条链,分别为 KUBE-SERVICES、KUBE-FIREWALL、KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-NODE-PORT、KUBE-MARK-DROP、KUBE-FORWARD、KUBE-LOAD-BALANCER ,如下所示: NAT 表: Filter 表: 此外,由于 linux 内核原生的 ipvs 模式只支持 DNAT,不支持 SNAT,所以,在以下几种场景中 ipvs 仍需要依赖 iptables 规则: 1、kube-proxy 启动时指定 –-masquerade-all=true 参数,即集群中所有经过 kube-proxy 的包都做一次 SNAT; 2、kube-proxy 启动时指定 --cluster-cidr= 参数; 3、对于 Load Balancer 类型的 service,用于配置白名单; 4、对于 NodePort 类型的 service,用于配置 MASQUERADE; 5、对于 externalIPs 类型的 service; 但对于 ipvs 模式的 kube-proxy,无论有多少 pod/service,iptables 的规则数都是固定的。 ipvs 模式的启用 1、首先要加载 IPVS 所需要的 kernel module $ modprobe -- ip_vs $ modprobe -- ip_vs_rr $ modprobe -- ip_vs_wrr $ modprobe -- ip_vs_sh $ modprobe -- nf_conntrack_ipv4 $ cut -f1 -d \" \" /proc/modules | grep -e ip_vs -e nf_conntrack_ipv4 2、在启动 kube-proxy 时,指定 proxy-mode 参数 --proxy-mode=ipvs (如果要使用其他负载均衡算法,可以指定 --ipvs-scheduler= 参数,默认为 rr) 当创建 ClusterIP type 的 service 时,IPVS proxier 会执行以下三个操作: 确保本机已创建 dummy 网卡,默认为 kube-ipvs0。为什么要创建 dummy 网卡?因为 ipvs netfilter 的 DNAT 钩子挂载在 INPUT 链上,当访问 ClusterIP 时,将 ClusterIP 绑定在 dummy 网卡上为了让内核识别该 IP 就是本机 IP,进而进入 INPUT 链,然后通过钩子函数 ip_vs_in 转发到 POSTROUTING 链; 将 ClusterIP 绑定到 dummy 网卡; 为每个 ClusterIP 创建 IPVS virtual servers 和 real server,分别对应 service 和 endpoints; 例如下面的示例: // kube-ipvs0 dummy 网卡 $ ip addr ...... 4: kube-ipvs0: mtu 1500 qdisc noop state DOWN group default link/ether de:be:c0:73:bc:c7 brd ff:ff:ff:ff:ff:ff inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0 valid_lft forever preferred_lft forever inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0 valid_lft forever preferred_lft forever inet 10.97.4.140/32 brd 10.97.4.140 scope global kube-ipvs0 valid_lft forever preferred_lft forever ...... // 10.97.4.140 为 CLUSTER-IP 挂载在 kube-ipvs0 上 $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE tenant-service ClusterIP 10.97.4.140 7000/TCP 23s // 10.97.4.140 后端的 realserver 分别为 10.244.1.2 和 10.244.1.3 $ ipvsadm -L -n IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.97.4.140:7000 rr -> 10.244.1.2:7000 Masq 1 0 0 -> 10.244.1.3:7000 Masq 1 0 0 ipvs 模式下数据包的流向 clusterIP 访问方式 PREROUTING --> KUBE-SERVICES --> KUBE-CLUSTER-IP --> INPUT --> KUBE-FIREWALL --> POSTROUTING 首先进入 PREROUTING 链 从 PREROUTING 链会转到 KUBE-SERVICES 链,10.244.0.0/16 为 ClusterIP 网段 在 KUBE-SERVICES 链打标记 从 KUBE-SERVICES 链再进入到 KUBE-CLUSTER-IP 链 KUBE-CLUSTER-IP 为 ipset 集合,在此处会进行 DNAT 然后会进入 INPUT 链 从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记 在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到 POSTROUTING 链 -A PREROUTING -m comment --comment \"kubernetes service portals\" -j KUBE-SERVICES -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment \"Kubernetes service cluster ip + port for masquerade purpose\" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ // 执行完 PREROUTING 规则,数据打上0x4000/0x4000的标记 -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT KUBE-CLUSTER-IP 为 ipset 列表: # ipset list | grep -A 20 KUBE-CLUSTER-IP Name: KUBE-CLUSTER-IP Type: hash:ip,port Revision: 5 Header: family inet hashsize 1024 maxelem 65536 Size in memory: 352 References: 2 Members: 10.96.0.10,17:53 10.96.0.10,6:53 10.96.0.1,6:443 10.96.0.10,6:9153 然后会进入 INPUT: -A INPUT -j KUBE-FIREWALL -A KUBE-FIREWALL -m comment --comment \"kubernetes firewall for dropping marked packets\" -m mark --mark 0x8000/0x8000 -j DROP 如果进来的数据带有 0x8000/0x8000 标记则丢弃,若有 0x4000/0x4000 标记则正常执行: -A POSTROUTING -m comment --comment \"kubernetes postrouting rules\" -j KUBE-POSTROUTING -A KUBE-POSTROUTING -m comment --comment \"kubernetes service traffic requiring SNAT\" -m mark --mark 0x4000/0x4000 -j MASQUERADE nodePort 方式 PREROUTING --> KUBE-SERVICES --> KUBE-NODE-PORT --> INPUT --> KUBE-FIREWALL --> POSTROUTING 首先进入 PREROUTING 链 从 PREROUTING 链会转到 KUBE-SERVICES 链 在 KUBE-SERVICES 链打标记 从 KUBE-SERVICES 链再进入到 KUBE-NODE-PORT 链 KUBE-NODE-PORT 为 ipset 集合,在此处会进行 DNAT 然后会进入 INPUT 链 从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记 在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到 POSTROUTING 链 -A PREROUTING -m comment --comment \"kubernetes service portals\" -j KUBE-SERVICES -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment \"Kubernetes service cluster ip + port for masquerade purpose\" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 -A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT KUBE-NODE-PORT 对应的 ipset 列表: # ipset list | grep -B 10 KUBE-NODE-PORT Name: KUBE-NODE-PORT-TCP Type: bitmap:port Revision: 3 Header: range 0-65535 Size in memory: 8268 References: 0 Members: 流入 INPUT 后与 ClusterIP 的访问方式相同。 kube-proxy ipvs 源码分析 kubernetes 版本:v1.16 在前面的文章中已经介绍过 ipvs 的初始化了,下面直接看其核心方法:proxier.syncRunner。 func NewProxier(......) { ...... proxier.syncRunner = async.NewBoundedFrequencyRunner(\"sync-runner\", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs) ...... } proxier.syncRunner() 执行流程: 通过 iptables-save 获取现有的 Filter 和 NAT 表存在的链数据 创建自定义链与规则 创建 Dummy 接口和 ipset 默认列表 为每个服务生成 ipvs 规则 对 serviceMap 内的每个服务进行遍历处理,对不同的服务类型(clusterip/nodePort/externalIPs/load-balancer)进行不同的处理(ipset 列表/vip/ipvs 后端服务器) 根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表 若为 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP 若为 externalIPs 类型更新对应的 ipset 列表 KUBE-EXTERNAL-IP 若为 load-balancer 类型更新对应的 ipset 列表 KUBE-LOAD-BALANCER、KUBE-LOAD-BALANCER-LOCAL、KUBE-LOAD-BALANCER-FW、KUBE-LOAD-BALANCER-SOURCE-CIDR、KUBE-LOAD-BALANCER-SOURCE-IP 若为 NodePort 类型更新对应的 ipset 列表 KUBE-NODE-PORT-TCP、KUBE-NODE-PORT-LOCAL-TCP、KUBE-NODE-PORT-LOCAL-SCTP-HASH、KUBE-NODE-PORT-LOCAL-UDP、KUBE-NODE-PORT-SCTP-HASH、KUBE-NODE-PORT-UDP 同步 ipset 记录 刷新 iptables 规则 func (proxier *Proxier) syncProxyRules() { proxier.mu.Lock() defer proxier.mu.Unlock() serviceUpdateResult := proxy.UpdateServiceMap(proxier.serviceMap, proxier.serviceChanges) endpointUpdateResult := proxier.endpointsMap.Update(proxier.endpointsChanges) staleServices := serviceUpdateResult.UDPStaleClusterIP // 合并 service 列表 for _, svcPortName := range endpointUpdateResult.StaleServiceNames { if svcInfo, ok := proxier.serviceMap[svcPortName]; ok && svcInfo != nil && svcInfo.Protocol() == v1.ProtocolUDP { staleServices.Insert(svcInfo.ClusterIP().String()) for _, extIP := range svcInfo.ExternalIPStrings() { staleServices.Insert(extIP) } } } ...... 读取系统 iptables 到内存,创建自定义链以及 iptables 规则,创建 dummy interface kube-ipvs0,创建默认的 ipset 规则。 proxier.natChains.Reset() proxier.natRules.Reset() proxier.filterChains.Reset() proxier.filterRules.Reset() writeLine(proxier.filterChains, \"*filter\") writeLine(proxier.natChains, \"*nat\") // 创建kubernetes的表连接链数据 proxier.createAndLinkeKubeChain() // 创建 dummy interface kube-ipvs0 _, err := proxier.netlinkHandle.EnsureDummyDevice(DefaultDummyDevice) if err != nil { ...... return } // 创建默认的 ipset 规则 for _, set := range proxier.ipsetList { if err := ensureIPSet(set); err != nil { return } set.resetEntries() } 对每一个服务创建 ipvs 规则。根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表。 for svcName, svc := range proxier.serviceMap { svcInfo, ok := svc.(*serviceInfo) if !ok { ...... } for _, e := range proxier.endpointsMap[svcName] { ep, ok := e.(*proxy.BaseEndpointInfo) if !ok { klog.Errorf(\"Failed to cast BaseEndpointInfo %q\", e.String()) continue } ...... if valid := proxier.ipsetList[kubeLoopBackIPSet].validateEntry(entry); !valid { ...... } proxier.ipsetList[kubeLoopBackIPSet].activeEntries.Insert(entry.String()) } 对于 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP。 if valid := proxier.ipsetList[kubeClusterIPSet].validateEntry(entry); !valid { ...... } proxier.ipsetList[kubeClusterIPSet].activeEntries.Insert(entry.String()) ...... if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { ...... } // 绑定 ClusterIP to dummy interface if err := proxier.syncService(svcNameString, serv, true); err == nil { // 同步 endpoints 信息 if err := proxier.syncEndpoint(svcName, false, serv); err != nil { ...... } } else { ...... } 为 externalIP 创建 ipvs 规则。 for _, externalIP := range svcInfo.ExternalIPStrings() { if local, err := utilproxy.IsLocalIP(externalIP); err != nil { ...... } else if local && (svcInfo.Protocol() != v1.ProtocolSCTP) { ...... if proxier.portsMap[lp] != nil { ...... } else { socket, err := proxier.portMapper.OpenLocalPort(&lp) if err != nil { ...... } replacementPortsMap[lp] = socket } } ...... if valid := proxier.ipsetList[kubeExternalIPSet].validateEntry(entry); !valid { ...... } proxier.ipsetList[kubeExternalIPSet].activeEntries.Insert(entry.String()) ...... if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { ...... } if err := proxier.syncService(svcNameString, serv, true); err == nil { ...... if err := proxier.syncEndpoint(svcName, false, serv); err != nil { ...... } } else { ...... } } 为 load-balancer类型创建 ipvs 规则。 for _, ingress := range svcInfo.LoadBalancerIPStrings() { if ingress != \"\" { ...... if valid := proxier.ipsetList[kubeLoadBalancerSet].validateEntry(entry); !valid { ...... } proxier.ipsetList[kubeLoadBalancerSet].activeEntries.Insert(entry.String()) if svcInfo.OnlyNodeLocalEndpoints() { ...... } if len(svcInfo.LoadBalancerSourceRanges()) != 0 { ...... for _, src := range svcInfo.LoadBalancerSourceRanges() { ...... } ...... } ...... if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { ...... } if err := proxier.syncService(svcNameString, serv, true); err == nil { ...... if err := proxier.syncEndpoint(svcName, svcInfo.OnlyNodeLocalEndpoints(), serv); err != nil { ...... } } else { ...... } } } 为 nodePort 类型创建 ipvs 规则。 if svcInfo.NodePort() != 0 { ...... var lps []utilproxy.LocalPort for _, address := range nodeAddresses { ...... lps = append(lps, lp) } for _, lp := range lps { if proxier.portsMap[lp] != nil { ...... } else if svcInfo.Protocol() != v1.ProtocolSCTP { socket, err := proxier.portMapper.OpenLocalPort(&lp) if err != nil { ...... } if lp.Protocol == \"udp\" { ...... } } } switch protocol { case \"tcp\": ...... case \"udp\": ...... case \"sctp\": ...... default: ...... } if nodePortSet != nil { for _, entry := range entries { ...... nodePortSet.activeEntries.Insert(entry.String()) } } if svcInfo.OnlyNodeLocalEndpoints() { var nodePortLocalSet *IPSet switch protocol { case \"tcp\": nodePortLocalSet = proxier.ipsetList[kubeNodePortLocalSetTCP] case \"udp\": nodePortLocalSet = proxier.ipsetList[kubeNodePortLocalSetUDP] case \"sctp\": nodePortLocalSet = proxier.ipsetList[kubeNodePortLocalSetSCTP] default: ...... } if nodePortLocalSet != nil { entryInvalidErr := false for _, entry := range entries { ...... nodePortLocalSet.activeEntries.Insert(entry.String()) } ...... } } for _, nodeIP := range nodeIPs { ...... if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP { ...... } if err := proxier.syncService(svcNameString, serv, false); err == nil { if err := proxier.syncEndpoint(svcName, svcInfo.OnlyNodeLocalEndpoints(), serv); err != nil { ...... } } else { ...... } } } } 同步 ipset 记录,清理 conntrack。 for _, set := range proxier.ipsetList { set.syncIPSetEntries() } proxier.writeIptablesRules() proxier.iptablesData.Reset() proxier.iptablesData.Write(proxier.natChains.Bytes()) proxier.iptablesData.Write(proxier.natRules.Bytes()) proxier.iptablesData.Write(proxier.filterChains.Bytes()) proxier.iptablesData.Write(proxier.filterRules.Bytes()) err = proxier.iptables.RestoreAll(proxier.iptablesData.Bytes(), utiliptables.NoFlushTables, utiliptables.RestoreCounters) if err != nil { ...... } ...... proxier.deleteEndpointConnections(endpointUpdateResult.StaleEndpoints) } 总结 本文主要讲述了 kube-proxy ipvs 模式的原理与实现,iptables 模式与 ipvs 模式下在源码实现上有许多相似之处,但二者原理不同,理解了原理分析代码则更加容易,笔者对于 ipvs 的知识也是现学的,文中如有不当之处望指正。虽然 ipvs 的性能要比 iptables 更好,但社区中已有相关的文章指出 BPF(Berkeley Packet Filter) 比 ipvs 的性能更好,且 BPF 将要取代 iptables,至于下一步如何发展,让我们拭目以待。 参考: http://www.austintek.com/LVS/LVS-HOWTO/HOWTO/LVS-HOWTO.filter_rules.html https://bestsamina.github.io/posts/2018-10-19-ipvs-based-kube-proxy-4-scaled-k8s-lb/ https://www.bookstack.cn/read/k8s-source-code-analysis/core-kube-proxy-ipvs.md https://blog.51cto.com/goome/2369150 https://xigang.github.io/2019/07/21/kubernetes-service/ https://segmentfault.com/a/1190000016333317 https://cilium.io/blog/2018/04/17/why-is-the-kernel-community-replacing-iptables/ Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kubelet-modules.html":{"url":"kubernetes/kubelet-modules.html","title":"kubelet 架构浅析","keywords":"","body":"一、概要 kubelet 是运行在每个节点上的主要的“节点代理”,每个节点都会启动 kubelet进程,用来处理 Master 节点下发到本节点的任务,按照 PodSpec 描述来管理Pod 和其中的容器(PodSpec 是用来描述一个 pod 的 YAML 或者 JSON 对象)。 kubelet 通过各种机制(主要通过 apiserver )获取一组 PodSpec 并保证在这些 PodSpec 中描述的容器健康运行。 二、kubelet 的主要功能 1、kubelet 默认监听四个端口,分别为 10250 、10255、10248、4194。 LISTEN 0 128 *:10250 *:* users:((\"kubelet\",pid=48500,fd=28)) LISTEN 0 128 *:10255 *:* users:((\"kubelet\",pid=48500,fd=26)) LISTEN 0 128 *:4194 *:* users:((\"kubelet\",pid=48500,fd=13)) LISTEN 0 128 127.0.0.1:10248 *:* users:((\"kubelet\",pid=48500,fd=23)) 10250(kubelet API):kubelet server 与 apiserver 通信的端口,定期请求 apiserver 获取自己所应当处理的任务,通过该端口可以访问获取 node 资源以及状态。 10248(健康检查端口):通过访问该端口可以判断 kubelet 是否正常工作, 通过 kubelet 的启动参数 --healthz-port 和 --healthz-bind-address 来指定监听的地址和端口。 $ curl http://127.0.0.1:10248/healthz ok 4194(cAdvisor 监听):kublet 通过该端口可以获取到该节点的环境信息以及 node 上运行的容器状态等内容,访问 http://localhost:4194 可以看到 cAdvisor 的管理界面,通过 kubelet 的启动参数 --cadvisor-port 可以指定启动的端口。 $ curl http://127.0.0.1:4194/metrics 10255 (readonly API):提供了 pod 和 node 的信息,接口以只读形式暴露出去,访问该端口不需要认证和鉴权。 // 获取 pod 的接口,与 apiserver 的 // http://127.0.0.1:8080/api/v1/pods?fieldSelector=spec.nodeName= 接口类似 $ curl http://127.0.0.1:10255/pods // 节点信息接口,提供磁盘、网络、CPU、内存等信息 $ curl http://127.0.0.1:10255/spec/ 2、kubelet 主要功能: pod 管理:kubelet 定期从所监听的数据源获取节点上 pod/container 的期望状态(运行什么容器、运行的副本数量、网络或者存储如何配置等等),并调用对应的容器平台接口达到这个状态。 容器健康检查:kubelet 创建了容器之后还要查看容器是否正常运行,如果容器运行出错,就要根据 pod 设置的重启策略进行处理。 容器监控:kubelet 会监控所在节点的资源使用情况,并定时向 master 报告,资源使用数据都是通过 cAdvisor 获取的。知道整个集群所有节点的资源情况,对于 pod 的调度和正常运行至关重要。 三、kubelet 组件中的模块 上图展示了 kubelet 组件中的模块以及模块间的划分。 1、PLEG(Pod Lifecycle Event Generator) PLEG 是 kubelet 的核心模块,PLEG 会一直调用 container runtime 获取本节点 containers/sandboxes 的信息,并与自身维护的 pods cache 信息进行对比,生成对应的 PodLifecycleEvent,然后输出到 eventChannel 中,通过 eventChannel 发送到 kubelet syncLoop 进行消费,然后由 kubelet syncPod 来触发 pod 同步处理过程,最终达到用户的期望状态。 2、cAdvisor cAdvisor(https://github.com/google/cadvisor)是 google 开发的容器监控工具,集成在 kubelet 中,起到收集本节点和容器的监控信息,大部分公司对容器的监控数据都是从 cAdvisor 中获取的 ,cAvisor 模块对外提供了 interface 接口,该接口也被 imageManager,OOMWatcher,containerManager 等所使用。 3、OOMWatcher 系统 OOM 的监听器,会与 cadvisor 模块之间建立 SystemOOM,通过 Watch方式从 cadvisor 那里收到的 OOM 信号,并产生相关事件。 4、probeManager probeManager 依赖于 statusManager,livenessManager,containerRefManager,会定时去监控 pod 中容器的健康状况,当前支持两种类型的探针:livenessProbe 和readinessProbe。 livenessProbe:用于判断容器是否存活,如果探测失败,kubelet 会 kill 掉该容器,并根据容器的重启策略做相应的处理。 readinessProbe:用于判断容器是否启动完成,将探测成功的容器加入到该 pod 所在 service 的 endpoints 中,反之则移除。readinessProbe 和 livenessProbe 有三种实现方式:http、tcp 以及 cmd。 5、statusManager statusManager 负责维护状态信息,并把 pod 状态更新到 apiserver,但是它并不负责监控 pod 状态的变化,而是提供对应的接口供其他组件调用,比如 probeManager。 6、containerRefManager 容器引用的管理,相对简单的Manager,用来报告容器的创建,失败等事件,通过定义 map 来实现了 containerID 与 v1.ObjectReferece 容器引用的映射。 7、evictionManager 当节点的内存、磁盘或 inode 等资源不足时,达到了配置的 evict 策略, node 会变为 pressure 状态,此时 kubelet 会按照 qosClass 顺序来驱赶 pod,以此来保证节点的稳定性。可以通过配置 kubelet 启动参数 --eviction-hard= 来决定 evict 的策略值。 8、imageGC imageGC 负责 node 节点的镜像回收,当本地的存放镜像的本地磁盘空间达到某阈值的时候,会触发镜像的回收,删除掉不被 pod 所使用的镜像,回收镜像的阈值可以通过 kubelet 的启动参数 --image-gc-high-threshold 和 --image-gc-low-threshold 来设置。 9、containerGC containerGC 负责清理 node 节点上已消亡的 container,具体的 GC 操作由runtime 来实现。 10、imageManager 调用 kubecontainer 提供的PullImage/GetImageRef/ListImages/RemoveImage/ImageStates 方法来保证pod 运行所需要的镜像。 11、volumeManager 负责 node 节点上 pod 所使用 volume 的管理,volume 与 pod 的生命周期关联,负责 pod 创建删除过程中 volume 的 mount/umount/attach/detach 流程,kubernetes 采用 volume Plugins 的方式,实现存储卷的挂载等操作,内置几十种存储插件。 12、containerManager 负责 node 节点上运行的容器的 cgroup 配置信息,kubelet 启动参数如果指定 --cgroups-per-qos 的时候,kubelet 会启动 goroutine 来周期性的更新 pod 的 cgroup 信息,维护其正确性,该参数默认为 true,实现了 pod 的Guaranteed/BestEffort/Burstable 三种级别的 Qos。 13、runtimeManager containerRuntime 负责 kubelet 与不同的 runtime 实现进行对接,实现对于底层 container 的操作,初始化之后得到的 runtime 实例将会被之前描述的组件所使用。可以通过 kubelet 的启动参数 --container-runtime 来定义是使用docker 还是 rkt,默认是 docker。 14、podManager podManager 提供了接口来存储和访问 pod 的信息,维持 static pod 和 mirror pods 的关系,podManager 会被statusManager/volumeManager/runtimeManager 所调用,podManager 的接口处理流程里面会调用 secretManager 以及 configMapManager。 在 v1.12 中,kubelet 组件有18个 manager: certificateManager cgroupManager containerManager cpuManager nodeContainerManager configmapManager containerReferenceManager evictionManager nvidiaGpuManager imageGCManager kuberuntimeManager hostportManager podManager proberManager secretManager statusManager volumeManager tokenManager 其中比较重要的模块后面会进行一一分析。 参考: 微软资深工程师详解 K8S 容器运行时 kubernetes 简介: kubelet 和 pod Kubelet 组件解析 Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kubelet_init.html":{"url":"kubernetes/kubelet_init.html","title":"kubelet 启动流程分析","keywords":"","body":"上篇文章(kubelet 架构浅析 )已经介绍过 kubelet 在整个集群架构中的功能以及自身各模块的用途,本篇文章主要介绍 kubelet 的启动流程。 kubernetes 版本: v1.12 kubelet 启动流程 kubelet 代码结构: ➜ kubernetes git:(release-1.12) ✗ tree cmd/kubelet cmd/kubelet ├── BUILD ├── OWNERS ├── app │ ├── BUILD │ ├── OWNERS │ ├── auth.go │ ├── init_others.go │ ├── init_windows.go │ ├── options │ │ ├── BUILD │ │ ├── container_runtime.go │ │ ├── globalflags.go │ │ ├── globalflags_linux.go │ │ ├── globalflags_other.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── osflags_others.go │ │ └── osflags_windows.go │ ├── plugins.go │ ├── server.go │ ├── server_linux.go │ ├── server_test.go │ └── server_unsupported.go └── kubelet.go 2 directories, 22 files 1、kubelet 入口函数 main(cmd/kubelet/kubelet.go) func main() { rand.Seed(time.Now().UTC().UnixNano()) command := app.NewKubeletCommand(server.SetupSignalHandler()) logs.InitLogs() defer logs.FlushLogs() if err := command.Execute(); err != nil { fmt.Fprintf(os.Stderr, \"%v\\n\", err) os.Exit(1) } } 2、初始化 kubelet 配置(cmd/kubelet/app/server.go) NewKubeletCommand() 函数主要负责获取配置文件中的参数,校验参数以及为参数设置默认值。 // NewKubeletCommand creates a *cobra.Command object with default parameters func NewKubeletCommand(stopCh 0 { kubeletConfig, err = loadConfigFile(configFile) if err != nil { glog.Fatal(err) } ... } // 校验 kubelet 参数 if err := kubeletconfigvalidation.ValidateKubeletConfiguration(kubeletConfig); err != nil { glog.Fatal(err) } ... // 此处初始化了 kubeletDeps kubeletDeps, err := UnsecuredDependencies(kubeletServer) if err != nil { glog.Fatal(err) } ... // 启动程序 if err := Run(kubeletServer, kubeletDeps, stopCh); err != nil { glog.Fatal(err) } }, } ... return cmd } kubeletDeps 包含 kubelet 运行所必须的配置,是为了实现 dependency injection,其目的是为了把 kubelet 依赖的组件对象作为参数传进来,这样可以控制 kubelet 的行为。主要包括监控功能(cadvisor),cgroup 管理功能(containerManager)等。 NewKubeletCommand() 会调用 Run() 函数,Run() 中主要调用 run() 函数进行一些准备事项。 3、创建和 apiserver 通信的对象(cmd/kubelet/app/server.go) run() 函数的主要功能: 1、创建 kubeClient,evnetClient 用来和 apiserver 通信。创建 heartbeatClient 向 apiserver 上报心跳状态。 2、为 kubeDeps 设定一些默认值。 3、启动监听 Healthz 端口的 http server,默认端口是 10248。 func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh 0 { healthz.DefaultHealthz() go wait.Until(func() { err := http.ListenAndServe(net.JoinHostPort(s.HealthzBindAddress, strconv.Itoa(int(s.HealthzPort))), nil) if err != nil { glog.Errorf(\"Starting health server failed: %v\", err) } }, 5*time.Second, wait.NeverStop) } ... } kubelet 对 pod 资源的获取方式有三种:第一种是通过文件获得,文件一般放在 /etc/kubernetes/manifests 目录下面;第二种也是通过文件过得,只不过文件是通过 URL 获取的;第三种是通过 watch kube-apiserver 获取。其中前两种模式下,我们称 kubelet 运行在 standalone 模式下,运行在 standalone 模式下的 kubelet 一般用于调试某些功能。 run() 中调用 RunKubelet() 函数进行后续操作。 4、初始化 kubelet 组件内部的模块(cmd/kubelet/app/server.go) RunKubelet() 主要功能: 1、初始化 kubelet 组件中的各个模块,创建出 kubelet 对象。 2、启动垃圾回收服务。 func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencies, runOnce bool) error { ... // 初始化 kubelet 内部模块 k, err := CreateAndInitKubelet(&kubeServer.KubeletConfiguration, kubeDeps, &kubeServer.ContainerRuntimeOptions, kubeServer.ContainerRuntime, kubeServer.RuntimeCgroups, kubeServer.HostnameOverride, kubeServer.NodeIP, kubeServer.ProviderID, kubeServer.CloudProvider, kubeServer.CertDirectory, kubeServer.RootDirectory, kubeServer.RegisterNode, kubeServer.RegisterWithTaints, kubeServer.AllowedUnsafeSysctls, kubeServer.RemoteRuntimeEndpoint, kubeServer.RemoteImageEndpoint, kubeServer.ExperimentalMounterPath, kubeServer.ExperimentalKernelMemcgNotification, kubeServer.ExperimentalCheckNodeCapabilitiesBeforeMount, kubeServer.ExperimentalNodeAllocatableIgnoreEvictionThreshold, kubeServer.MinimumGCAge, kubeServer.MaxPerPodContainerCount, kubeServer.MaxContainerCount, kubeServer.MasterServiceNamespace, kubeServer.RegisterSchedulable, kubeServer.NonMasqueradeCIDR, kubeServer.KeepTerminatedPodVolumes, kubeServer.NodeLabels, kubeServer.SeccompProfileRoot, kubeServer.BootstrapCheckpointPath, kubeServer.NodeStatusMaxImages) if err != nil { return fmt.Errorf(\"failed to create kubelet: %v\", err) } ... if runOnce { if _, err := k.RunOnce(podCfg.Updates()); err != nil { return fmt.Errorf(\"runonce failed: %v\", err) } glog.Infof(\"Started kubelet as runonce\") } else { // startKubelet(k, podCfg, &kubeServer.KubeletConfiguration, kubeDeps, kubeServer.EnableServer) glog.Infof(\"Started kubelet\") } } func CreateAndInitKubelet(...){ // NewMainKubelet 实例化一个 kubelet 对象,并对 kubelet 内部各个模块进行初始化 k, err = kubelet.NewMainKubelet(kubeCfg, kubeDeps, crOptions, containerRuntime, runtimeCgroups, hostnameOverride, nodeIP, providerID, cloudProvider, certDirectory, rootDirectory, registerNode, registerWithTaints, allowedUnsafeSysctls, remoteRuntimeEndpoint, remoteImageEndpoint, experimentalMounterPath, experimentalKernelMemcgNotification, experimentalCheckNodeCapabilitiesBeforeMount, experimentalNodeAllocatableIgnoreEvictionThreshold, minimumGCAge, maxPerPodContainerCount, maxContainerCount, masterServiceNamespace, registerSchedulable, nonMasqueradeCIDR, keepTerminatedPodVolumes, nodeLabels, seccompProfileRoot, bootstrapCheckpointPath, nodeStatusMaxImages) if err != nil { return nil, err } // 通知 apiserver kubelet 启动了 k.BirthCry() // 启动垃圾回收服务 k.StartGarbageCollection() return k, nil } func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,...){ ... if kubeDeps.PodConfig == nil { var err error // 初始化 makePodSourceConfig,监听 pod 元数据的来源(FILE, URL, api-server),将不同 source 的 pod configuration 合并到一个结构中 kubeDeps.PodConfig, err = makePodSourceConfig(kubeCfg, kubeDeps, nodeName, bootstrapCheckpointPath) if err != nil { return nil, err } } // kubelet 服务端口,默认 10250 daemonEndpoints := &v1.NodeDaemonEndpoints{ KubeletEndpoint: v1.DaemonEndpoint{Port: kubeCfg.Port}, } // 使用 reflector 把 ListWatch 得到的服务信息实时同步到 serviceStore 对象中 serviceIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) if kubeDeps.KubeClient != nil { serviceLW := cache.NewListWatchFromClient(kubeDeps.KubeClient.CoreV1().RESTClient(), \"services\", metav1.NamespaceAll, fields.Everything()) r := cache.NewReflector(serviceLW, &v1.Service{}, serviceIndexer, 0) go r.Run(wait.NeverStop) } serviceLister := corelisters.NewServiceLister(serviceIndexer) // 使用 reflector 把 ListWatch 得到的节点信息实时同步到 nodeStore 对象中 nodeIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) if kubeDeps.KubeClient != nil { fieldSelector := fields.Set{api.ObjectNameField: string(nodeName)}.AsSelector() nodeLW := cache.NewListWatchFromClient(kubeDeps.KubeClient.CoreV1().RESTClient(), \"nodes\", metav1.NamespaceAll, fieldSelector) r := cache.NewReflector(nodeLW, &v1.Node{}, nodeIndexer, 0) go r.Run(wait.NeverStop) } nodeInfo := &predicates.CachedNodeInfo{NodeLister: corelisters.NewNodeLister(nodeIndexer)} ... // node 资源不足时的驱逐策略的设定 thresholds, err := eviction.ParseThresholdConfig(enforceNodeAllocatable, kubeCfg.EvictionHard, kubeCfg.EvictionSoft, kubeCfg.EvictionSoftGracePeriod, kubeCfg.EvictionMinimumReclaim) if err != nil { return nil, err } evictionConfig := eviction.Config{ PressureTransitionPeriod: kubeCfg.EvictionPressureTransitionPeriod.Duration, MaxPodGracePeriodSeconds: int64(kubeCfg.EvictionMaxPodGracePeriod), Thresholds: thresholds, KernelMemcgNotification: experimentalKernelMemcgNotification, PodCgroupRoot: kubeDeps.ContainerManager.GetPodCgroupRoot(), } ... // 容器引用的管理 containerRefManager := kubecontainer.NewRefManager() // oom 监控 oomWatcher := NewOOMWatcher(kubeDeps.CAdvisorInterface, kubeDeps.Recorder) // 根据配置信息和各种对象创建 Kubelet 实例 klet := &Kubelet{ hostname: hostname, hostnameOverridden: len(hostnameOverride) > 0, nodeName: nodeName, ... } // 从 cAdvisor 获取当前机器的信息 machineInfo, err := klet.cadvisor.MachineInfo() // 对 pod 的管理(如: 增删改等) klet.podManager = kubepod.NewBasicPodManager(kubepod.NewBasicMirrorClient(klet.kubeClient), secretManager, configMapManager, checkpointManager) // 容器运行时管理 runtime, err := kuberuntime.NewKubeGenericRuntimeManager(...) // pleg klet.pleg = pleg.NewGenericPLEG(klet.containerRuntime, plegChannelCapacity, plegRelistPeriod, klet.podCache, clock.RealClock{}) // 创建 containerGC 对象,进行周期性的容器清理工作 containerGC, err := kubecontainer.NewContainerGC(klet.containerRuntime, containerGCPolicy, klet.sourcesReady) // 创建 imageManager 管理镜像 imageManager, err := images.NewImageGCManager(klet.containerRuntime, klet.StatsProvider, kubeDeps.Recorder, nodeRef, imageGCPolicy, crOptions.PodSandboxImage) // statusManager 实时检测节点上 pod 的状态,并更新到 apiserver 对应的 pod klet.statusManager = status.NewManager(klet.kubeClient, klet.podManager, klet) // 探针管理 klet.probeManager = prober.NewManager(...) // token 管理 tokenManager := token.NewManager(kubeDeps.KubeClient) // 磁盘管理 klet.volumeManager = volumemanager.NewVolumeManager() // 将 syncPod() 注入到 podWorkers 中 klet.podWorkers = newPodWorkers(klet.syncPod, kubeDeps.Recorder, klet.workQueue, klet.resyncInterval, backOffPeriod, klet.podCache) // 容器驱逐策略管理 evictionManager, evictionAdmitHandler := eviction.NewManager(klet.resourceAnalyzer, evictionConfig, killPodNow(klet.podWorkers, kubeDeps.Recorder), klet.imageManager, klet.containerGC, kubeDeps.Recorder, nodeRef, klet.clock) ... } RunKubelet 最后会调用 startKubelet() 进行后续的操作。 5、启动 kubelet 内部的模块及服务(cmd/kubelet/app/server.go) startKubelet() 的主要功能: 1、以 goroutine 方式启动 kubelet 中的各个模块。 2、启动 kubelet http server。 func startKubelet(k kubelet.Bootstrap, podCfg *config.PodConfig, kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *kubelet.Dependencies, enableServer bool) { go wait.Until(func() { // 以 goroutine 方式启动 kubelet 中的各个模块 k.Run(podCfg.Updates()) }, 0, wait.NeverStop) // 启动 kubelet http server if enableServer { go k.ListenAndServe(net.ParseIP(kubeCfg.Address), uint(kubeCfg.Port), kubeDeps.TLSOptions, kubeDeps.Auth, kubeCfg.EnableDebuggingHandlers, kubeCfg.EnableContentionProfiling) } if kubeCfg.ReadOnlyPort > 0 { go k.ListenAndServeReadOnly(net.ParseIP(kubeCfg.Address), uint(kubeCfg.ReadOnlyPort)) } } // Run starts the kubelet reacting to config updates func (kl *Kubelet) Run(updates syncLoop 是 kubelet 的主循环方法,它从不同的管道(FILE,URL, API-SERVER)监听 pod 的变化,并把它们汇聚起来。当有新的变化发生时,它会调用对应的函数,保证 Pod 处于期望的状态。 func (kl *Kubelet) syncLoop(updates syncLoopIteration() 方法对多个管道进行遍历,如果 pod 发生变化,则会调用相应的 Handler,在 Handler 中通过调用 dispatchWork 分发任务。 总结 本篇文章主要讲述了 kubelet 组件从加载配置到初始化内部的各个模块再到启动 kubelet 服务的整个流程,上面的时序图能清楚的看到函数之间的调用关系,但是其中每个组件具体的工作方式以及组件之间的交互方式还不得而知,后面会一探究竟。 参考: kubernetes node components – kubelet Kubelet 源码分析(一):启动流程分析 kubelet 源码分析:启动流程 kubernetes 的 kubelet 的工作过程 kubelet 内部实现解析 Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/kubelet_create_pod.html":{"url":"kubernetes/kubelet_create_pod.html","title":"kubelet 创建 pod 的流程","keywords":"","body":"上篇文章介绍了 kubelet 的启动流程,本篇文章主要介绍 kubelet 创建 pod 的流程。 kubernetes 版本: v1.12 kubelet 的工作核心就是在围绕着不同的生产者生产出来的不同的有关 pod 的消息来调用相应的消费者(不同的子模块)完成不同的行为(创建和删除 pod 等),即图中的控制循环(SyncLoop),通过不同的事件驱动这个控制循环运行。 本文仅分析新建 pod 的流程,当一个 pod 完成调度,与一个 node 绑定起来之后,这个 pod 就会触发 kubelet 在循环控制里注册的 handler,上图中的 HandlePods 部分。此时,通过检查 pod 在 kubelet 内存中的状态,kubelet 就能判断出这是一个新调度过来的 pod,从而触发 Handler 里的 ADD 事件对应的逻辑处理。然后 kubelet 会为这个 pod 生成对应的 podStatus,接着检查 pod 所声明的 volume 是不是准备好了,然后调用下层的容器运行时。如果是 update 事件的话,kubelet 就会根据 pod 对象具体的变更情况,调用下层的容器运行时进行容器的重建。 kubelet 创建 pod 的流程 1、kubelet 的控制循环(syncLoop) syncLoop 中首先定义了一个 syncTicker 和 housekeepingTicker,即使没有需要更新的 pod 配置,kubelet 也会定时去做同步和清理 pod 的工作。然后在 for 循环中一直调用 syncLoopIteration,如果在每次循环过程中出现比较严重的错误,kubelet 会记录到 runtimeState 中,遇到错误就等待 5 秒中继续循环。 func (kl *Kubelet) syncLoop(updates 2、监听 pod 变化(syncLoopIteration) syncLoopIteration 这个方法就会对多个管道进行遍历,发现任何一个管道有消息就交给 handler 去处理。它会从以下管道中获取消息: configCh:该信息源由 kubeDeps 对象中的 PodConfig 子模块提供,该模块将同时 watch 3 个不同来源的 pod 信息的变化(file,http,apiserver),一旦某个来源的 pod 信息发生了更新(创建/更新/删除),这个 channel 中就会出现被更新的 pod 信息和更新的具体操作。 syncCh:定时器管道,每隔一秒去同步最新保存的 pod 状态 houseKeepingCh:housekeeping 事件的管道,做 pod 清理工作 plegCh:该信息源由 kubelet 对象中的 pleg 子模块提供,该模块主要用于周期性地向 container runtime 查询当前所有容器的状态,如果状态发生变化,则这个 channel 产生事件。 livenessManager.Updates():健康检查发现某个 pod 不可用,kubelet 将根据 Pod 的restartPolicy 自动执行正确的操作 func (kl *Kubelet) syncLoopIteration(configCh 3、处理新增 pod(HandlePodAddtions) 对于事件中的每个 pod,执行以下操作: 1、把所有的 pod 按照创建日期进行排序,保证最先创建的 pod 会最先被处理 2、把它加入到 podManager 中,podManager 子模块负责管理这台机器上的 pod 的信息,pod 和 mirrorPod 之间的对应关系等等。所有被管理的 pod 都要出现在里面,如果 podManager 中找不到某个 pod,就认为这个 pod 被删除了 3、如果是 mirror pod 调用其单独的方法 4、验证 pod 是否能在该节点运行,如果不可以直接拒绝 5、通过 dispatchWork 把创建 pod 的工作下发给 podWorkers 子模块做异步处理 6、在 probeManager 中添加 pod,如果 pod 中定义了 readiness 和 liveness 健康检查,启动 goroutine 定期进行检测 func (kl *Kubelet) HandlePodAdditions(pods []*v1.Pod) { start := kl.clock.Now() // 对所有 pod 按照日期排序,保证最先创建的 pod 优先被处理 sort.Sort(sliceutils.PodsByCreationTime(pods)) for _, pod := range pods { if kl.dnsConfigurer != nil && kl.dnsConfigurer.ResolverConfig != \"\" { kl.dnsConfigurer.CheckLimitsForResolvConf() } existingPods := kl.podManager.GetPods() // 把 pod 加入到 podManager 中 kl.podManager.AddPod(pod) // 判断是否是 mirror pod(即 static pod) if kubepod.IsMirrorPod(pod) { kl.handleMirrorPod(pod, start) continue } if !kl.podIsTerminated(pod) { activePods := kl.filterOutTerminatedPods(existingPods) // 通过 canAdmitPod 方法校验Pod能否在该计算节点创建(如:磁盘空间) // Check if we can admit the pod; if not, reject it. if ok, reason, message := kl.canAdmitPod(activePods, pod); !ok { kl.rejectPod(pod, reason, message) continue } } mirrorPod, _ := kl.podManager.GetMirrorPodByPod(pod) // 通过 dispatchWork 分发 pod 做异步处理,dispatchWork 主要工作就是把接收到的参数封装成 UpdatePodOptions,调用 UpdatePod 方法. kl.dispatchWork(pod, kubetypes.SyncPodCreate, mirrorPod, start) // 在 probeManager 中添加 pod,如果 pod 中定义了 readiness 和 liveness 健康检查,启动 goroutine 定期进行检测 kl.probeManager.AddPod(pod) } } static pod 是由 kubelet 直接管理的,k8s apiserver 并不会感知到 static pod 的存在,当然也不会和任何一个 rs 关联上,完全是由 kubelet 进程来监管,并在它异常时负责重启。Kubelet 会通过 apiserver 为每一个 static pod 创建一个对应的 mirror pod,如此以来就可以可以通过 kubectl 命令查看对应的 pod,并且可以通过 kubectl logs 命令直接查看到static pod 的日志信息。 4、下发任务(dispatchWork) dispatchWorker 的主要作用是把某个对 Pod 的操作(创建/更新/删除)下发给 podWorkers。 func (kl *Kubelet) dispatchWork(pod *v1.Pod, syncType kubetypes.SyncPodType, mirrorPod *v1.Pod, start time.Time) { if kl.podIsTerminated(pod) { if pod.DeletionTimestamp != nil { kl.statusManager.TerminatePod(pod) } return } // 落实在 podWorkers 中 kl.podWorkers.UpdatePod(&UpdatePodOptions{ Pod: pod, MirrorPod: mirrorPod, UpdateType: syncType, OnCompleteFunc: func(err error) { if err != nil { metrics.PodWorkerLatency.WithLabelValues(syncType.String()).Observe(metrics.SinceInMicroseconds(start)) } }, }) if syncType == kubetypes.SyncPodCreate { metrics.ContainersPerPodCount.Observe(float64(len(pod.Spec.Containers))) } } 5、更新事件的 channel(UpdatePod) podWorkers 子模块主要的作用就是处理针对每一个的 Pod 的更新事件,比如 Pod 的创建,删除,更新。而 podWorkers 采取的基本思路是:为每一个 Pod 都单独创建一个 goroutine 和更新事件的 channel,goroutine 会阻塞式的等待 channel 中的事件,并且对获取的事件进行处理。而 podWorkers 对象自身则主要负责对更新事件进行下发。 func (p *podWorkers) UpdatePod(options *UpdatePodOptions) { pod := options.Pod uid := pod.UID var podUpdates chan UpdatePodOptions var exists bool p.podLock.Lock() defer p.podLock.Unlock() // 如果当前 pod 还没有启动过 goroutine ,则启动 goroutine,并且创建 channel if podUpdates, exists = p.podUpdates[uid]; !exists { // 创建 channel podUpdates = make(chan UpdatePodOptions, 1) p.podUpdates[uid] = podUpdates // 启动 goroutine go func() { defer runtime.HandleCrash() p.managePodLoop(podUpdates) }() } // 下发更新事件 if !p.isWorking[pod.UID] { p.isWorking[pod.UID] = true podUpdates 6、调用 syncPodFn 方法同步 pod(managePodLoop) managePodLoop 调用 syncPodFn 方法去同步 pod,syncPodFn 实际上就是kubelet.SyncPod。在完成这次 sync 动作之后,会调用 wrapUp 函数,这个函数将会做几件事情: 将这个 pod 信息插入 kubelet 的 workQueue 队列中,等待下一次周期性的对这个 pod 的状态进行 sync 将在这次 sync 期间堆积的没有能够来得及处理的最近一次 update 操作加入 goroutine 的事件 channel 中,立即处理。 func (p *podWorkers) managePodLoop(podUpdates 7、完成创建容器前的准备工作(SyncPod) 在这个方法中,主要完成以下几件事情: 如果是删除 pod,立即执行并返回 同步 podStatus 到 kubelet.statusManager 检查 pod 是否能运行在本节点,主要是权限检查(是否能使用主机网络模式,是否可以以 privileged 权限运行等)。如果没有权限,就删除本地旧的 pod 并返回错误信息 创建 containerManagar 对象,并且创建 pod level cgroup,更新 Qos level cgroup 如果是 static Pod,就创建或者更新对应的 mirrorPod 创建 pod 的数据目录,存放 volume 和 plugin 信息,如果定义了 pv,等待所有的 volume mount 完成(volumeManager 会在后台做这些事情),如果有 image secrets,去 apiserver 获取对应的 secrets 数据 然后调用 kubelet.volumeManager 组件,等待它将 pod 所需要的所有外挂的 volume 都准备好。 调用 container runtime 的 SyncPod 方法,去实现真正的容器创建逻辑 这里所有的事情都和具体的容器没有关系,可以看到该方法是创建 pod 实体(即容器)之前需要完成的准备工作。 func (kl *Kubelet) syncPod(o syncPodOptions) error { // pull out the required options pod := o.pod mirrorPod := o.mirrorPod podStatus := o.podStatus updateType := o.updateType // 是否为 删除 pod if updateType == kubetypes.SyncPodKill { ... } ... // 检查 pod 是否能运行在本节点 runnable := kl.canRunPod(pod) if !runnable.Admit { ... } // 更新 pod 状态 kl.statusManager.SetPodStatus(pod, apiPodStatus) // 如果 pod 非 running 状态则直接 kill 掉 if !runnable.Admit || pod.DeletionTimestamp != nil || apiPodStatus.Phase == v1.PodFailed { ... } // 加载网络插件 if rs := kl.runtimeState.networkErrors(); len(rs) != 0 && !kubecontainer.IsHostNetworkPod(pod) { ... } pcm := kl.containerManager.NewPodContainerManager() if !kl.podIsTerminated(pod) { ... // 创建并更新 pod 的 cgroups if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever) { if !pcm.Exists(pod) { ... } } } // 为 static pod 创建对应的 mirror pod if kubepod.IsStaticPod(pod) { ... } // 创建数据目录 if err := kl.makePodDataDirs(pod); err != nil { ... } // 挂载 volume if !kl.podIsTerminated(pod) { if err := kl.volumeManager.WaitForAttachAndMount(pod); err != nil { ... } } // 获取 secret 信息 pullSecrets := kl.getPullSecretsForPod(pod) // 调用 containerRuntime 的 SyncPod 方法开始创建容器 result := kl.containerRuntime.SyncPod(pod, apiPodStatus, podStatus, pullSecrets, kl.backOff) kl.reasonCache.Update(pod.UID, result) if err := result.Error(); err != nil { ... } return nil } 8、创建容器 containerRuntime(pkg/kubelet/kuberuntime)子模块的 SyncPod 函数才是真正完成 pod 内容器实体的创建。 syncPod 主要执行以下几个操作: 1、计算 sandbox 和 container 是否发生变化 2、创建 sandbox 容器 3、启动 init 容器 4、启动业务容器 initContainers 可以有多个,多个 container 严格按照顺序启动,只有当前一个 container 退出了以后,才开始启动下一个 container。 func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, _ v1.PodStatus, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) { // 1、计算 sandbox 和 container 是否发生变化 podContainerChanges := m.computePodActions(pod, podStatus) if podContainerChanges.CreateSandbox { ref, err := ref.GetReference(legacyscheme.Scheme, pod) if err != nil { glog.Errorf(\"Couldn't make a ref to pod %q: '%v'\", format.Pod(pod), err) } ... } // 2、kill 掉 sandbox 已经改变的 pod if podContainerChanges.KillPod { ... } else { // 3、kill 掉非 running 状态的 containers ... for containerID, containerInfo := range podContainerChanges.ContainersToKill { ... if err := m.killContainer(pod, containerID, containerInfo.name, containerInfo.message, nil); err != nil { ... } } } m.pruneInitContainersBeforeStart(pod, podStatus) podIP := \"\" if podStatus != nil { podIP = podStatus.IP } // 4、创建 sandbox podSandboxID := podContainerChanges.SandboxID if podContainerChanges.CreateSandbox { podSandboxID, msg, err = m.createPodSandbox(pod, podContainerChanges.Attempt) if err != nil { ... } ... podSandboxStatus, err := m.runtimeService.PodSandboxStatus(podSandboxID) if err != nil { ... } // 如果 pod 网络是 host 模式,容器也相同;其他情况下,容器会使用 None 网络模式,让 kubelet 的网络插件自己进行网络配置 if !kubecontainer.IsHostNetworkPod(pod) { podIP = m.determinePodSandboxIP(pod.Namespace, pod.Name, podSandboxStatus) glog.V(4).Infof(\"Determined the ip %q for pod %q after sandbox changed\", podIP, format.Pod(pod)) } } configPodSandboxResult := kubecontainer.NewSyncResult(kubecontainer.ConfigPodSandbox, podSandboxID) result.AddSyncResult(configPodSandboxResult) // 获取 PodSandbox 的配置(如:metadata,clusterDNS,容器的端口映射等) podSandboxConfig, err := m.generatePodSandboxConfig(pod, podContainerChanges.Attempt) ... // 5、启动 init container if container := podContainerChanges.NextInitContainerToStart; container != nil { ... if msg, err := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeInit); err != nil { ... } } // 6、启动业务容器 for _, idx := range podContainerChanges.ContainersToStart { ... if msg, err := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP, kubecontainer.ContainerTypeRegular); err != nil { ... } } return } 9、启动容器 最终由 startContainer 完成容器的启动,其主要有以下几个步骤: 1、拉取镜像 2、生成业务容器的配置信息 3、调用 docker api 创建容器 4、启动容器 5、执行 post start hook func (m *kubeGenericRuntimeManager) startContainer(podSandboxID string, podSandboxConfig *runtimeapi.PodSandboxConfig, container *v1.Container, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP string, containerType kubecontainer.ContainerType) (string, error) { // 1、检查业务镜像是否存在,不存在则到 Docker Registry 或是 Private Registry 拉取镜像。 imageRef, msg, err := m.imagePuller.EnsureImageExists(pod, container, pullSecrets) if err != nil { ... } ref, err := kubecontainer.GenerateContainerRef(pod, container) if err != nil { ... } // 设置 RestartCount restartCount := 0 containerStatus := podStatus.FindContainerStatusByName(container.Name) if containerStatus != nil { restartCount = containerStatus.RestartCount + 1 } // 2、生成业务容器的配置信息 containerConfig, cleanupAction, err := m.generateContainerConfig(container, pod, restartCount, podIP, imageRef, containerType) if cleanupAction != nil { defer cleanupAction() } ... // 3、通过 client.CreateContainer 调用 docker api 创建业务容器 containerID, err := m.runtimeService.CreateContainer(podSandboxID, containerConfig, podSandboxConfig) if err != nil { ... } err = m.internalLifecycle.PreStartContainer(pod, container, containerID) if err != nil { ... } ... // 3、启动业务容器 err = m.runtimeService.StartContainer(containerID) if err != nil { ... } containerMeta := containerConfig.GetMetadata() sandboxMeta := podSandboxConfig.GetMetadata() legacySymlink := legacyLogSymlink(containerID, containerMeta.Name, sandboxMeta.Name, sandboxMeta.Namespace) containerLog := filepath.Join(podSandboxConfig.LogDirectory, containerConfig.LogPath) if _, err := m.osInterface.Stat(containerLog); !os.IsNotExist(err) { if err := m.osInterface.Symlink(containerLog, legacySymlink); err != nil { glog.Errorf(\"Failed to create legacy symbolic link %q to container %q log %q: %v\", legacySymlink, containerID, containerLog, err) } } // 4、执行 post start hook if container.Lifecycle != nil && container.Lifecycle.PostStart != nil { kubeContainerID := kubecontainer.ContainerID{ Type: m.runtimeName, ID: containerID, } // runner.Run 这个方法的主要作用就是在业务容器起来的时候, // 首先会执行一个 container hook(PostStart 和 PreStop),做一些预处理工作。 // 只有 container hook 执行成功才会运行具体的业务服务,否则容器异常。 msg, handlerErr := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart) if handlerErr != nil { ... } } return \"\", nil } 总结 本文主要讲述了 kubelet 从监听到容器调度至本节点再到创建容器的一个过程,kubelet 最终调用 docker api 来创建容器的。结合上篇文章,可以看出 kubelet 从启动到创建 pod 的一个清晰过程。 参考: k8s源码分析-kubelet Kubelet源码分析(一):启动流程分析 kubelet 源码分析:pod 新建流程 kubelet创建Pod流程解析 Kubelet: Pod Lifecycle Event Generator (PLEG) Design- proposals Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/node_status.html":{"url":"kubernetes/node_status.html","title":"kubelet 状态上报的方式","keywords":"","body":"分布式系统中服务端会通过心跳机制确认客户端是否存活,在 k8s 中,kubelet 也会定时上报心跳到 apiserver,以此判断该 node 是否存活,若 node 超过一定时间没有上报心跳,其状态会被置为 NotReady,宿主上容器的状态也会被置为 Nodelost 或者 Unknown 状态。kubelet 自身会定期更新状态到 apiserver,通过参数 --node-status-update-frequency 指定上报频率,默认是 10s 上报一次,kubelet 不止上报心跳信息还会上报自身的一些数据信息。 一、kubelet 上报哪些状态 在 k8s 中,一个 node 的状态包含以下几个信息: Addresses Condition Capacity Info 1、Addresses 主要包含以下几个字段: HostName:Hostname 。可以通过 kubelet 的 --hostname-override 参数进行覆盖。 ExternalIP:通常是可以外部路由的 node IP 地址(从集群外可访问)。 InternalIP:通常是仅可在集群内部路由的 node IP 地址。 2、Condition conditions 字段描述了所有 Running nodes 的状态。 3、Capacity 描述 node 上的可用资源:CPU、内存和可以调度到该 node 上的最大 pod 数量。 4、Info 描述 node 的一些通用信息,例如内核版本、Kubernetes 版本(kubelet 和 kube-proxy 版本)、Docker 版本 (如果使用了)和系统版本,这些信息由 kubelet 从 node 上获取到。 使用 kubectl get node xxx -o yaml 可以看到 node 所有的状态的信息,其中 status 中的信息都是 kubelet 需要上报的,所以 kubelet 不止上报心跳信息还上报节点信息、节点 OOD 信息、内存磁盘压力状态、节点监控状态、是否调度等。 二、kubelet 状态异常时的影响 如果一个 node 处于非 Ready 状态超过 pod-eviction-timeout的值(默认为 5 分钟,在 kube-controller-manager 中定义),在 v1.5 之前的版本中 kube-controller-manager 会 force delete pod 然后调度该宿主上的 pods 到其他宿主,在 v1.5 之后的版本中,kube-controller-manager 不会 force delete pod,pod 会一直处于Terminating 或Unknown 状态直到 node 被从 master 中删除或 kubelet 状态变为 Ready。在 node NotReady 期间,Daemonset 的 Pod 状态变为 Nodelost,Deployment、Statefulset 和 Static Pod 的状态先变为 NodeLost,然后马上变为 Unknown。Deployment 的 pod 会 recreate,Static Pod 和 Statefulset 的 Pod 会一直处于 Unknown 状态。 当 kubelet 变为 Ready 状态时,Daemonset的pod不会recreate,旧pod状态直接变为Running,Deployment的则是将kubelet进程停止的Node删除,Statefulset的Pod会重新recreate,Staic Pod 会被删除。 三、kubelet 状态上报的实现 kubelet 有两种上报状态的方式,第一种定期向 apiserver 发送心跳消息,简单理解就是启动一个 goroutine 然后定期向 APIServer 发送消息。 第二中被称为 NodeLease,在 v1.13 之前的版本中,节点的心跳只有 NodeStatus,从 v1.13 开始,NodeLease feature 作为 alpha 特性引入。当启用 NodeLease feature 时,每个节点在“kube-node-lease”名称空间中都有一个关联的“Lease”对象,该对象由节点定期更新,NodeStatus 和 NodeLease 都被视为来自节点的心跳。NodeLease 会频繁更新,而只有在 NodeStatus 发生改变或者超过了一定时间(默认值为1分钟,node-monitor-grace-period 的默认值为 40s),才会将 NodeStatus 上报给 master。由于 NodeLease 比 NodeStatus 更轻量级,该特性在集群规模扩展性和性能上有明显提升。本文主要分析第一种上报方式的实现。 kubernetes 版本 :v1.13 kubelet 上报状态的代码大部分在 kubernetes/pkg/kubelet/kubelet_node_status.go 中实现。状态上报的功能是在 kubernetes/pkg/kubelet/kubelet.go#Run 方法以 goroutine 形式中启动的,kubelet 中多个重要的功能都是在该方法中启动的。 kubernetes/pkg/kubelet/kubelet.go#Run func (kl *Kubelet) Run(updates kl.syncNodeStatus 便是上报状态的,此处 kl.nodeStatusUpdateFrequency 使用的是默认设置的 10s,也就是说节点间同步状态的函数 kl.syncNodeStatus 每 10s 执行一次。 syncNodeStatus 是状态上报的入口函数,其后所调用的多个函数也都是在同一个文件中实现的。 kubernetes/pkg/kubelet/kubelet_node_status.go#syncNodeStatus func (kl *Kubelet) syncNodeStatus() { kl.syncNodeStatusMux.Lock() defer kl.syncNodeStatusMux.Unlock() if kl.kubeClient == nil || kl.heartbeatClient == nil { return } // 是否为注册节点 if kl.registerNode { // This will exit immediately if it doesn't need to do anything. kl.registerWithAPIServer() } if err := kl.updateNodeStatus(); err != nil { klog.Errorf(\"Unable to update node status: %v\", err) } } syncNodeStatus 调用 updateNodeStatus, 然后又调用 tryUpdateNodeStatus 来进行上报操作,而最终调用的是 setNodeStatus。这里还进行了同步状态判断,如果是注册节点,则执行 registerWithAPIServer,否则,执行 updateNodeStatus。 updateNodeStatus 主要是调用 tryUpdateNodeStatus 进行后续的操作,该函数中定义了状态上报重试的次数,nodeStatusUpdateRetry 默认定义为 5 次。 kubernetes/pkg/kubelet/kubelet_node_status.go#updateNodeStatus func (kl *Kubelet) updateNodeStatus() error { klog.V(5).Infof(\"Updating node status\") for i := 0; i 0 && kl.onRepeatedHeartbeatFailure != nil { kl.onRepeatedHeartbeatFailure() } klog.Errorf(\"Error updating node status, will retry: %v\", err) } else { return nil } } return fmt.Errorf(\"update node status exceeds retry count\") } tryUpdateNodeStatus 是主要的上报逻辑,先给 node 设置状态,然后上报 node 的状态到 master。 kubernetes/pkg/kubelet/kubelet_node_status.go#tryUpdateNodeStatus func (kl *Kubelet) tryUpdateNodeStatus(tryNumber int) error { opts := metav1.GetOptions{} if tryNumber == 0 { util.FromApiserverCache(&opts) } // 获取 node 信息 node, err := kl.heartbeatClient.CoreV1().Nodes().Get(string(kl.nodeName), opts) if err != nil { return fmt.Errorf(\"error getting node %q: %v\", kl.nodeName, err) } originalNode := node.DeepCopy() if originalNode == nil { return fmt.Errorf(\"nil %q node object\", kl.nodeName) } podCIDRChanged := false if node.Spec.PodCIDR != \"\" { if podCIDRChanged, err = kl.updatePodCIDR(node.Spec.PodCIDR); err != nil { klog.Errorf(err.Error()) } } // 设置 node 状态 kl.setNodeStatus(node) now := kl.clock.Now() if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) && now.Before(kl.lastStatusReportTime.Add(kl.nodeStatusReportFrequency)) { if !podCIDRChanged && !nodeStatusHasChanged(&originalNode.Status, &node.Status) { kl.volumeManager.MarkVolumesAsReportedInUse(node.Status.VolumesInUse) return nil } } // 更新 node 信息到 master // Patch the current status on the API server updatedNode, _, err := nodeutil.PatchNodeStatus(kl.heartbeatClient.CoreV1(), types.NodeName(kl.nodeName), originalNode, node) if err != nil { return err } kl.lastStatusReportTime = now kl.setLastObservedNodeAddresses(updatedNode.Status.Addresses) // If update finishes successfully, mark the volumeInUse as reportedInUse to indicate // those volumes are already updated in the node's status kl.volumeManager.MarkVolumesAsReportedInUse(updatedNode.Status.VolumesInUse) return nil } tryUpdateNodeStatus 中调用 setNodeStatus 设置 node 的状态。setNodeStatus 会获取一次 node 的所有状态,然后会将 kubelet 中保存的所有状态改为最新的值,也就是会重置 node status 中的所有字段。 kubernetes/pkg/kubelet/kubelet_node_status.go#setNodeStatus func (kl *Kubelet) setNodeStatus(node *v1.Node) { for i, f := range kl.setNodeStatusFuncs { klog.V(5).Infof(\"Setting node status at position %v\", i) if err := f(node); err != nil { klog.Warningf(\"Failed to set some node status fields: %s\", err) } } } setNodeStatus 通过 setNodeStatusFuncs 方法覆盖 node 结构体中所有的字段,setNodeStatusFuncs 是在 NewMainKubelet(pkg/kubelet/kubelet.go) 中初始化的。 kubernetes/pkg/kubelet/kubelet.go#NewMainKubelet func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, // ... // Generating the status funcs should be the last thing we do, klet.setNodeStatusFuncs = klet.defaultNodeStatusFuncs() return klet, nil } defaultNodeStatusFuncs 是生成状态的函数,通过获取 node 的所有状态指标后使用工厂函数生成状态 kubernetes/pkg/kubelet/kubelet_node_status.go#defaultNodeStatusFuncs func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error { // if cloud is not nil, we expect the cloud resource sync manager to exist var nodeAddressesFunc func() ([]v1.NodeAddress, error) if kl.cloud != nil { nodeAddressesFunc = kl.cloudResourceSyncManager.NodeAddresses } var validateHostFunc func() error if kl.appArmorValidator != nil { validateHostFunc = kl.appArmorValidator.ValidateHost } var setters []func(n *v1.Node) error setters = append(setters, nodestatus.NodeAddress(kl.nodeIP, kl.nodeIPValidator, kl.hostname, kl.hostnameOverridden, kl.externalCloudProvider, kl.cloud, nodeAddressesFunc), nodestatus.MachineInfo(string(kl.nodeName), kl.maxPods, kl.podsPerCore, kl.GetCachedMachineInfo, kl.containerManager.GetCapacity, kl.containerManager.GetDevicePluginResourceCapacity, kl.containerManager.GetNodeAllocatableReservation, kl.recordEvent), nodestatus.VersionInfo(kl.cadvisor.VersionInfo, kl.containerRuntime.Type, kl.containerRuntime.Version), nodestatus.DaemonEndpoints(kl.daemonEndpoints), nodestatus.Images(kl.nodeStatusMaxImages, kl.imageManager.GetImageList), nodestatus.GoRuntime(), ) if utilfeature.DefaultFeatureGate.Enabled(features.AttachVolumeLimit) { setters = append(setters, nodestatus.VolumeLimits(kl.volumePluginMgr.ListVolumePluginWithLimits)) } setters = append(setters, nodestatus.MemoryPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderMemoryPressure, kl.recordNodeStatusEvent), nodestatus.DiskPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderDiskPressure, kl.recordNodeStatusEvent), nodestatus.PIDPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderPIDPressure, kl.recordNodeStatusEvent), nodestatus.ReadyCondition(kl.clock.Now, kl.runtimeState.runtimeErrors, kl.runtimeState.networkErrors, kl.runtimeState.storageErrors, validateHostFunc, kl.containerManager. Status, kl.recordNodeStatusEvent), nodestatus.VolumesInUse(kl.volumeManager.ReconcilerStatesHasBeenSynced, kl.volumeManager.GetVolumesInUse), nodestatus.RemoveOutOfDiskCondition(), // TODO(mtaufen): I decided not to move this setter for now, since all it does is send an event // and record state back to the Kubelet runtime object. In the future, I'd like to isolate // these side-effects by decoupling the decisions to send events and partial status recording // from the Node setters. kl.recordNodeSchedulableEvent, ) return setters } defaultNodeStatusFuncs 可以看到 node 上报的所有信息,主要有 MemoryPressureCondition、DiskPressureCondition、PIDPressureCondition、ReadyCondition 等。每一种 nodestatus 都返回一个 setters,所有 setters 的定义在 pkg/kubelet/nodestatus/setters.go 文件中。 对于二次开发而言,如果我们需要 APIServer 掌握更多的 Node 信息,可以在此处添加自定义函数,例如,上报磁盘信息等。 tryUpdateNodeStatus 中最后调用 PatchNodeStatus 上报 node 的状态到 master。 kubernetes/pkg/util/node/node.go#PatchNodeStatus // PatchNodeStatus patches node status. func PatchNodeStatus(c v1core.CoreV1Interface, nodeName types.NodeName, oldNode *v1.Node, newNode *v1.Node) (*v1.Node, []byte, error) { // 计算 patch patchBytes, err := preparePatchBytesforNodeStatus(nodeName, oldNode, newNode) if err != nil { return nil, nil, err } updatedNode, err := c.Nodes().Patch(string(nodeName), types.StrategicMergePatchType, patchBytes, \"status\") if err != nil { return nil, nil, fmt.Errorf(\"failed to patch status %q for node %q: %v\", patchBytes, nodeName, err) } return updatedNode, patchBytes, nil } 在 PatchNodeStatus 会调用已注册的那些方法将状态把状态发给 APIServer。 四、总结 本文主要讲述了 kubelet 上报状态的方式及其实现,node 状态上报的方式目前有两种,本文仅分析了第一种状态上报的方式。在大规模集群中由于节点数量比较多,所有 node 都频繁报状态对 etcd 会有一定的压力,当 node 与 master 通信时由于网络导致心跳上报失败也会影响 node 的状态,为了避免类似问题的出现才有 NodeLease 方式,对于该功能的实现后文会继续进行分析。 参考: https://www.qikqiak.com/post/kubelet-sync-node-status/ https://www.jianshu.com/p/054450557818 https://blog.csdn.net/shida_csdn/article/details/84286058 https://kubernetes.io/docs/concepts/architecture/nodes/ Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "},"kubernetes/k8s_events.html":{"url":"kubernetes/k8s_events.html","title":"kubelet 中事件处理机制","keywords":"","body":"当集群中的 node 或 pod 异常时,大部分用户会使用 kubectl 查看对应的 events,那么 events 是从何而来的?其实 k8s 中的各个组件会将运行时产生的各种事件汇报到 apiserver,对于 k8s 中的可描述资源,使用 kubectl describe 都可以看到其相关的 events,那 k8s 中又有哪几个组件都上报 events 呢? 只要在 k8s.io/kubernetes/cmd 目录下暴力搜索一下就能知道哪些组件会产生 events: $ grep -R -n -i \"EventRecorder\" . 可以看出,controller-manage、kube-proxy、kube-scheduler、kubelet 都使用了 EventRecorder,本文只讲述 kubelet 中对 Events 的使用。 1、Events 的定义 events 在 k8s.io/api/core/v1/types.go 中进行定义,结构体如下所示: type Event struct { metav1.TypeMeta `json:\",inline\"` metav1.ObjectMeta `json:\"metadata\" protobuf:\"bytes,1,opt,name=metadata\"` InvolvedObject ObjectReference `json:\"involvedObject\" protobuf:\"bytes,2,opt,name=involvedObject\"` Reason string `json:\"reason,omitempty\" protobuf:\"bytes,3,opt,name=reason\"` Message string `json:\"message,omitempty\" protobuf:\"bytes,4,opt,name=message\"` Source EventSource `json:\"source,omitempty\" protobuf:\"bytes,5,opt,name=source\"` FirstTimestamp metav1.Time `json:\"firstTimestamp,omitempty\" protobuf:\"bytes,6,opt,name=firstTimestamp\"` LastTimestamp metav1.Time `json:\"lastTimestamp,omitempty\" protobuf:\"bytes,7,opt,name=lastTimestamp\"` Count int32 `json:\"count,omitempty\" protobuf:\"varint,8,opt,name=count\"` Type string `json:\"type,omitempty\" protobuf:\"bytes,9,opt,name=type\"` EventTime metav1.MicroTime `json:\"eventTime,omitempty\" protobuf:\"bytes,10,opt,name=eventTime\"` Series *EventSeries `json:\"series,omitempty\" protobuf:\"bytes,11,opt,name=series\"` Action string `json:\"action,omitempty\" protobuf:\"bytes,12,opt,name=action\"` Related *ObjectReference `json:\"related,omitempty\" protobuf:\"bytes,13,opt,name=related\"` ReportingController string `json:\"reportingComponent\" protobuf:\"bytes,14,opt,name=reportingComponent\"` ReportingInstance string `json:\"reportingInstance\" protobuf:\"bytes,15,opt,name=reportingInstance\"` ReportingInstance string `json:\"reportingInstance\" protobuf:\"bytes,15,opt,name=reportingInstance\"` } 其中 InvolvedObject 代表和事件关联的对象,source 代表事件源,使用 kubectl 看到的事件一般包含 Type、Reason、Age、From、Message 几个字段。 k8s 中 events 目前只有两种类型:\"Normal\" 和 \"Warning\": 2、EventBroadcaster 的初始化 events 的整个生命周期都与 EventBroadcaster 有关,kubelet 中对 EventBroadcaster 的初始化在k8s.io/kubernetes/cmd/kubelet/app/server.go中: func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencies, runOnce bool) error { ... // event 初始化 makeEventRecorder(kubeDeps, nodeName) ... } func makeEventRecorder(kubeDeps *kubelet.Dependencies, nodeName types.NodeName) { if kubeDeps.Recorder != nil { return } // 初始化 EventBroadcaster eventBroadcaster := record.NewBroadcaster() // 初始化 EventRecorder kubeDeps.Recorder = eventBroadcaster.NewRecorder(legacyscheme.Scheme, v1.EventSource{Component: componentKubelet, Host: string(nodeName)}) // 记录 events 到本地日志 eventBroadcaster.StartLogging(glog.V(3).Infof) if kubeDeps.EventClient != nil { glog.V(4).Infof(\"Sending events to api server.\") // 上报 events 到 apiserver eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: kubeDeps.EventClient.Events(\"\")}) } else { glog.Warning(\"No api server defined - no events will be sent to API server.\") } } Kubelet 在启动的时候会初始化一个 EventBroadcaster,它主要是对接收到的 events 做一些后续的处理(保存、上报等),EventBroadcaster 也会被 kubelet 中的其他模块使用,以下是相关的定义,对 events 生成和处理的函数都定义在 k8s.io/client-go/tools/record/event.go 中: type eventBroadcasterImpl struct { *watch.Broadcaster sleepDuration time.Duration } // EventBroadcaster knows how to receive events and send them to any EventSink, watcher, or log. type EventBroadcaster interface { StartEventWatcher(eventHandler func(*v1.Event)) watch.Interface StartRecordingToSink(sink EventSink) watch.Interface StartLogging(logf func(format string, args ...interface{})) watch.Interface NewRecorder(scheme *runtime.Scheme, source v1.EventSource) EventRecorder } EventBroadcaster 是个接口类型,该接口有以下四个方法: StartEventWatcher() : EventBroadcaster 中的核心方法,接收各模块产生的 events,参数为一个处理 events 的函数,用户可以使用 StartEventWatcher() 接收 events 然后使用自定义的 handle 进行处理 StartRecordingToSink() : 调用 StartEventWatcher() 接收 events,并将收到的 events 发送到 apiserver StartLogging() :也是调用 StartEventWatcher() 接收 events,然后保存 events 到日志 NewRecorder() :会创建一个指定 EventSource 的 EventRecorder,EventSource 指明了哪个节点的哪个组件 eventBroadcasterImpl 是 eventBroadcaster 实际的对象,初始化 EventBroadcaster 对象的时候会初始化一个 Broadcaster,Broadcaster 会启动一个 goroutine 接收各组件产生的 events 并广播到每一个 watcher。 func NewBroadcaster() EventBroadcaster { return &eventBroadcasterImpl{watch.NewBroadcaster(maxQueuedEvents, watch.DropIfChannelFull), defaultSleepDuration} } 可以看到,kubelet 在初始化完 EventBroadcaster 后会调用 StartRecordingToSink() 和 StartLogging() 两个方法,StartRecordingToSink() 处理函数会将收到的 events 进行缓存、过滤、聚合而后发送到 apiserver,StartLogging() 仅将 events 保存到 kubelet 的日志中。 3、Events 的生成 从初始化 EventBroadcaster 的代码中可以看到 kubelet 在初始化完 EventBroadcaster 后紧接着初始化了 EventRecorder,并将已经初始化的 Broadcaster 对象作为参数传给了 EventRecorder,至此,EventBroadcaster、EventRecorder、Broadcaster 三个对象产生了关联。EventRecorder 的主要功能是生成指定格式的 events,以下是相关的定义: type recorderImpl struct { scheme *runtime.Scheme source v1.EventSource *watch.Broadcaster clock clock.Clock } type EventRecorder interface { Event(object runtime.Object, eventtype, reason, message string) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) PastEventf(object runtime.Object, timestamp metav1.Time, eventtype, reason, messageFmt string, args ...interface{}) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) } EventRecorder 中包含的几个方法都是产生指定格式的 events,Event() 和 Eventf() 的功能类似 fmt.Println() 和 fmt.Printf(),kubelet 中的各个模块会调用 EventRecorder 生成 events。recorderImpl 是 EventRecorder 实际的对象。EventRecorder 的每个方法会调用 generateEvent,在 generateEvent 中初始化 events 。 以下是生成 events 的函数: func (recorder *recorderImpl) generateEvent(object runtime.Object, annotations map[string]string, timestamp metav1.Time, eventtype, reason, message string) { ref, err := ref.GetReference(recorder.scheme, object) if err != nil { glog.Errorf(\"Could not construct reference to: '%#v' due to: '%v'. Will not report event: '%v' '%v' '%v'\", object, err, eventtype, reason, message) return } if !validateEventType(eventtype) { glog.Errorf(\"Unsupported event type: '%v'\", eventtype) return } event := recorder.makeEvent(ref, annotations, eventtype, reason, message) event.Source = recorder.source go func() { // NOTE: events should be a non-blocking operation defer utilruntime.HandleCrash() // 发送事件 recorder.Action(watch.Added, event) }() } func (recorder *recorderImpl) makeEvent(ref *v1.ObjectReference, annotations map[string]string, eventtype, reason, message string) *v1.Event { t := metav1.Time{Time: recorder.clock.Now()} namespace := ref.Namespace if namespace == \"\" { namespace = metav1.NamespaceDefault } return &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf(\"%v.%x\", ref.Name, t.UnixNano()), Namespace: namespace, Annotations: annotations, }, InvolvedObject: *ref, Reason: reason, Message: message, FirstTimestamp: t, LastTimestamp: t, Count: 1, Type: eventtype, } } 初始化完 events 后会调用 recorder.Action() 将 events 发送到 Broadcaster 的事件接收队列中, Action() 是 Broadcaster 中的方法。 以下是 Action() 方法的实现: func (m *Broadcaster) Action(action EventType, obj runtime.Object) { m.incoming 4、Events 的广播 上面已经说了,EventBroadcaster 初始化时会初始化一个 Broadcaster,Broadcaster 的作用就是接收所有的 events 并进行广播,Broadcaster 的实现在 k8s.io/apimachinery/pkg/watch/mux.go 中,Broadcaster 初始化完成后会在后台启动一个 goroutine,然后接收所有从 EventRecorder 发送过来的 events,Broadcaster 中有一个 map 会保存每一个注册的 watcher, 接着将 events 广播给所有的 watcher,每个 watcher 都有一个接收消息的 channel,watcher 可以通过它的 ResultChan() 方法从 channel 中读取数据进行消费。 以下是 Broadcaster 广播 events 的实现: func (m *Broadcaster) loop() { for event := range m.incoming { if event.Type == internalRunFunctionMarker { event.Object.(functionFakeRuntimeObject)() continue } m.distribute(event) } m.closeAll() m.distributing.Done() } // distribute sends event to all watchers. Blocking. func (m *Broadcaster) distribute(event Event) { m.lock.Lock() defer m.lock.Unlock() if m.fullChannelBehavior == DropIfChannelFull { for _, w := range m.watchers { select { case w.result 5、Events 的处理 那么 watcher 是从何而来呢?每一个要处理 events 的 client 都需要初始化一个 watcher,处理 events 的方法是在 EventBroadcaster 中定义的,以下是 EventBroadcaster 中对 events 处理的三个函数: func (eventBroadcaster *eventBroadcasterImpl) StartEventWatcher(eventHandler func(*v1.Event)) watch.Interface { watcher := eventBroadcaster.Watch() go func() { defer utilruntime.HandleCrash() for watchEvent := range watcher.ResultChan() { event, ok := watchEvent.Object.(*v1.Event) if !ok { // This is all local, so there's no reason this should // ever happen. continue } eventHandler(event) } }() return watcher } StartEventWatcher() 首先实例化一个 watcher,每个 watcher 都会被塞入到 Broadcaster 的 watcher 列表中,watcher 从 Broadcaster 提供的 channel 中读取 events,然后再调用 eventHandler 进行处理,StartLogging() 和 StartRecordingToSink() 都是对 StartEventWatcher() 的封装,都会传入自己的处理函数。 func (eventBroadcaster *eventBroadcasterImpl) StartLogging(logf func(format string, args ...interface{})) watch.Interface { return eventBroadcaster.StartEventWatcher( func(e *v1.Event) { logf(\"Event(%#v): type: '%v' reason: '%v' %v\", e.InvolvedObject, e.Type, e.Reason, e.Message) }) } StartLogging() 传入的 eventHandler 仅将 events 保存到日志中。 func (eventBroadcaster *eventBroadcasterImpl) StartRecordingToSink(sink EventSink) watch.Interface { // The default math/rand package functions aren't thread safe, so create a // new Rand object for each StartRecording call. randGen := rand.New(rand.NewSource(time.Now().UnixNano())) eventCorrelator := NewEventCorrelator(clock.RealClock{}) return eventBroadcaster.StartEventWatcher( func(event *v1.Event) { recordToSink(sink, event, eventCorrelator, randGen, eventBroadcaster.sleepDuration) }) } func recordToSink(sink EventSink, event *v1.Event, eventCorrelator *EventCorrelator, randGen *rand.Rand, sleepDuration time.Duration) { eventCopy := *event event = &eventCopy result, err := eventCorrelator.EventCorrelate(event) if err != nil { utilruntime.HandleError(err) } if result.Skip { return } tries := 0 for { if recordEvent(sink, result.Event, result.Patch, result.Event.Count > 1, eventCorrelator) { break } tries++ if tries >= maxTriesPerEvent { glog.Errorf(\"Unable to write event '%#v' (retry limit exceeded!)\", event) break } // 第一次重试增加随机性,防止 apiserver 重启的时候所有的事件都在同一时间发送事件 if tries == 1 { time.Sleep(time.Duration(float64(sleepDuration) * randGen.Float64())) } else { time.Sleep(sleepDuration) } } } StartRecordingToSink() 方法先根据当前时间生成一个随机数发生器 randGen,增加随机数是为了在重试时增加随机性,防止 apiserver 重启的时候所有的事件都在同一时间发送事件,接着实例化一个EventCorrelator,EventCorrelator 会对事件做一些预处理的工作,其中包括过滤、聚合、缓存等操作,具体代码不做详细分析,最后将 recordToSink() 函数作为处理函数,recordToSink() 会将处理后的 events 发送到 apiserver,这是 StartEventWatcher() 的整个工作流程。 6、Events 简单实现 了解完 events 的整个处理流程后,可以参考其实现方式写一个 demo,要实现一个完整的 events 需要包含以下几个功能: 1、事件的产生 2、事件的发送 3、事件广播 4、事件缓存 5、事件过滤和聚合 package main import ( \"fmt\" \"sync\" \"time\" ) // watcher queue const queueLength = int64(1) // Events xxx type Events struct { Reason string Message string Source string Type string Count int64 Timestamp time.Time } // EventBroadcaster xxx type EventBroadcaster interface { Event(etype, reason, message string) StartLogging() Interface Stop() } // eventBroadcaster xxx type eventBroadcasterImpl struct { *Broadcaster } func NewEventBroadcaster() EventBroadcaster { return &eventBroadcasterImpl{NewBroadcaster(queueLength)} } func (eventBroadcaster *eventBroadcasterImpl) Stop() { eventBroadcaster.Shutdown() } // generate event func (eventBroadcaster *eventBroadcasterImpl) Event(etype, reason, message string) { events := &Events{Type: etype, Reason: reason, Message: message} // send event to broadcast eventBroadcaster.Action(events) } // 仅实现 StartLogging() 的功能,将日志打印 func (eventBroadcaster *eventBroadcasterImpl) StartLogging() Interface { // register a watcher watcher := eventBroadcaster.Watch() go func() { for watchEvent := range watcher.ResultChan() { fmt.Printf(\"%v\\n\", watchEvent) } }() go func() { time.Sleep(time.Second * 4) watcher.Stop() }() return watcher } // -------------------- // Broadcaster 定义与实现 // 接收 events channel 的长度 const incomingQueuLength = 100 type Broadcaster struct { lock sync.Mutex incoming chan Events watchers map[int64]*broadcasterWatcher watchersQueue int64 watchQueueLength int64 distributing sync.WaitGroup } func NewBroadcaster(queueLength int64) *Broadcaster { m := &Broadcaster{ incoming: make(chan Events, incomingQueuLength), watchers: map[int64]*broadcasterWatcher{}, watchQueueLength: queueLength, } m.distributing.Add(1) // 后台启动一个 goroutine 广播 events go m.loop() return m } // Broadcaster 接收所产生的 events func (m *Broadcaster) Action(event *Events) { m.incoming 此处仅简单实现,将 EventRecorder 处理 events 的功能直接放在了 EventBroadcaster 中实现,对 events 的处理方法仅实现了 StartLogging(),Broadcaster 中的部分功能是直接复制 k8s 中的代码,有一定的精简,其实现值得学习,此处对 EventCorrelator 并没有进行实现。 代码请参考:https://github.com/gosoon/k8s-learning-notes/tree/master/k8s-package/events 7、总结 本文讲述了 k8s 中 events 从产生到展示的一个完整过程,最后也实现了一个简单的 demo,在此将 kubelet 对 events 的整个处理过程再梳理下,其中主要有三个对象 EventBroadcaster、EventRecorder、Broadcaster: 1、kubelet 首先会初始化 EventBroadcaster 对象,同时会初始化一个 Broadcaster 对象。 2、kubelet 通过 EventBroadcaster 对象的 NewRecorder() 方法初始化 EventRecorder 对象,EventRecorder 对象提供的几个方法会生成 events 并通过 Action() 方法发送 events 到 Broadcaster 的 channel 队列中。 3、Broadcaster 的作用就是接收所有的 events 并进行广播,Broadcaster 初始化后会在后台启动一个 goroutine,然后接收所有从 EventRecorder 发来的 events。 4、EventBroadcaster 对 events 有三个处理方法:StartEventWatcher()、StartRecordingToSink()、StartLogging(),StartEventWatcher() 是其中的核心方法,会初始化一个 watcher 注册到 Broadcaster,其余两个处理函数对 StartEventWatcher() 进行了封装,并实现了自己的处理函数。 5、 Broadcaster 中有一个 map 会保存每一个注册的 watcher,其会将所有的 events 广播给每一个 watcher,每个 watcher 通过它的 ResultChan() 方法从 channel 接收 events。 6、kubelet 会使用 StartRecordingToSink() 和 StartLogging() 对 events 进行处理,StartRecordingToSink() 处理函数收到 events 后会进行缓存、过滤、聚合而后发送到 apiserver,apiserver 会将 events 保存到 etcd 中,使用 kubectl 或其他客户端可以查看。StartLogging() 仅将 events 保存到 kubelet 的日志中。 Copyright © tianfeiyu 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:10:27 "}}