在大量使用蓝盾「Docker公共构建机」来跑构建任务后,我们发现拉起的构建 Pod 通过 hostPath 挂载工作目录做缓存,当同一流水线任务重复执行时能够加速,本文介绍蓝盾调度器如何进行调度到有缓存的节点。
1. 背景 在前文蓝盾「Docker公共构建机」缓存清理 中我们通过分析源码,知道拉起的构建 Pod 通过 hostPath 挂载工作目录做缓存。我们接下来进一步分析创建 Pod 的流程。
2. 部署配置 dispatch-k8s-manager/resources/config.yaml
dispatch: label: bkci.dispatch.kubenetes/core watch: task: label: bkci.dispatch.kubenetes/watch-task builder: nodeSelector: label: value: nodesAnnotation: bkci.dispatch.kubenetes/builder-history-nodes realResource: prometheusUrl: realResourceAnnotation: bkci.dispatch.kubenetes/builder-real-resources specialMachine: label: bkci.dispatch.kubenetes/special-builder privateMachine: label: bkci.dispatch.kubenetes/private-builder
通过 dispatch-k8s-manager
模块的配置文件,我们发现可以通过 nodeSelector
、 nodesAnnotation
、realResource
等配置来设置调度策略。
3. 源码分析 3.1 亲和性和污点容忍 dispatch-k8s-manager/pkg/apiserver/service/builder_start.go
func CreateBuilder (builder *Builder) (taskId string , err error) { volumes, volumeMounts := getBuilderVolumeAndMount(builder.Name, builder.NFSs) var replicas int32 = 1 tolers, nodeMatches := buildDedicatedBuilder(builder) ... annotations, err := getBuilderAnnotations(builder.Name) if err != nil { return "" , err } ... go task.DoCreateBuilder( taskId, &kubeclient.Deployment{ Name: builder.Name, Labels: labels, MatchLabels: matchlabels, Replicas: &replicas, Pod: kubeclient.Pod{ Labels: labels, Annotations: annotations, Volumes: volumes, Containers: []kubeclient.Container{ { Image: builder.Image, Resources: *resources, Env: getEnvs(builder.Env), Command: builder.Command, VolumeMounts: volumeMounts, }, }, NodeMatches: nodeMatches, Tolerations: tolers, PullImageSecret: pullImageSecret, }, }, ) return taskId, nil } func buildDedicatedBuilder (builder *Builder) ([]corev1.Toleration, []kubeclient.NodeMatch) { ... ... ... return nil , nil } func getBuilderAnnotations (builderName string ) (map [string ]string , error) { ... ... ... return result, nil }
dispatch-k8s-manager/pkg/kubeclient/deployment.go
func CreateDeployment (dep *Deployment) error { ... var affinity *corev1.Affinity if len (dep.Pod.NodeMatches) > 0 { var matches []corev1.NodeSelectorRequirement for _, mat := range dep.Pod.NodeMatches { matches = append (matches, corev1.NodeSelectorRequirement{ Key: mat.Key, Operator: mat.Operator, Values: mat.Values, }) } affinity = &corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: matches, }, }, }, }, } } ... return nil }
在 CreateBuilder 里,调度相关的两个核心参数 tolers 和 nodeMatches 都是通过 buildDedicatedBuilder(builder) 返回的,这两个参数会一起传递给 kubeclient 层,在 kubeclient 的 CreateDeployment 方法中:
NodeMatches 会被转换为 affinity.nodeAffinity,用于节点亲和调度。 Tolerations 会直接下发到 Pod 的 spec.tolerations 字段,用于污点容忍。 3.2 历史节点调度 蓝盾源码里我们找到了有关亲和性以及污点容忍的实现,但是有关历史节点调度的实现只有通过 getBuilderAnnotations 给 Pod 设置注解。至于如何通过注解影响调度在蓝盾源码里并没有找到相关内容。我们进一步分析发现,历史节点调度需要通过蓝盾基于K8S调度插件 实现。
apiVersion: v1 kind: Pod metadata: annotations: bkci.dispatch.kubenetes/builder-history-nodes: '["10.x.x.1","10.x.x.2","10.x.x.3"]' labels: bkci.dispatch.kubenetes/core: build1753761077695-ivcpmoxg bkci.dispatch.kubenetes/watch-task: t-1753785688231121886-iInjpMUr-builder-start name: build1753761077695-ivcpmoxg-c9d8fc6c9-mqhkk ...
package bkdevopsschedulerpluginimport ( "context" "encoding/json" "k8s.io/api/core/v1" "k8s.io/kubernetes/pkg/scheduler/framework" ) const nodesAnnotation = "bkci.dispatch.kubenetes/builder-history-nodes" const readResourceAnnotation = "bkci.dispatch.kubenetes/builder-real-resources" type realResourceUsage struct { Cpu string `json:"cpu"` Memory string `json:"memory"` } func (s *SchedulerPlugin) Score (_ context.Context, _ *framework.CycleState, pod *v1.Pod, nodeName string ) (int64 , *framework.Status) { var nodeHis []string if nodesS, ok := pod.ObjectMeta.Annotations[nodesAnnotation]; ok { _ = json.Unmarshal([]byte (nodesS), &nodeHis) } var realResources []realResourceUsage if realS, ok := pod.ObjectMeta.Annotations[readResourceAnnotation]; ok { _ = json.Unmarshal([]byte (realS), &realResources) } nodeScore := calculateNodeHisScore(nodeHis, nodeName) realResourceScore := ... return nodeScore + realResourceScore, nil } var nodeHisScores = map [int ]int64 {0 : 30 , 1 : 20 , 2 : 10 }func calculateNodeHisScore (nodeHis []string , nodeName string ) int64 { if len (nodeHis) == 0 { return framework.MinNodeScore } for index, name := range nodeHis { if name != nodeName { continue } score := framework.MinNodeScore if indexS, ok := nodeHisScores[index]; ok { score = indexS } return score } return framework.MinNodeScore }
在插件的 Score 阶段,会读取 Pod 的 bkci.dispatch.kubenetes/builder-history-nodes
注解内容,并将其反序列化为历史节点名称数组,即提供历史节点信息。 插件通过 calculateNodeHisScore 方法,根据当前调度节点是否在历史节点列表中,以及其在列表中的顺序,给予不同的分数(最近的历史节点分数最高)。 该分数会与资源分数(通过 bkci.dispatch.kubenetes/builder-real-resources
注解和节点资源情况计算得出)相加,作为最终调度优先级,影响调度器选择节点的排序。 4. 总结 在蓝盾流水线中,通过以下方式实现了 Kubernetes 的调度优化:
历史节点调度:通过注解记录历史节点信息,调度插件优先选择这些节点,减少初始化时间。 亲和性(Affinity):根据配置文件中的 nodeSelector 和代码中的 NodeMatches 转换为 nodeAffinity,确保 Pod 调度到特定节点。 污点容忍(Tolerations):仅在配置文件中指定了专机(privateMachine)时,生成污点容忍配置,允许 Pod 调度到带特定污点的节点。 这些机制协同提升了调度效率和资源利用率。
5. 参考