Kubernetes的NodeLifecycleController如何管理污点

前言

关于污点请阅读《污点和容忍度》了解基础概念,便于理解本文关于NodeLifecycleController如何管理节点污点的内容。

需要强调的是,本文的污点指的是NoExecute污点,这种污点会驱逐已经运行的pod,这可以从NoExecute字面意思看的出来。《kube-scheduler调度插件》的TaintToleration在Filter阶段会过滤掉pod无法容忍的NoSchedule和NoExecute污点,也就是说调度器不会调度pod到无法容忍污点node上,那NodeLifecycleController管理还要管理NoExecute干什么呢?道理很简单,node的污点会更新,pod的容忍度也会更新,调度器不会因为node的污点以及pod的容忍度的更新重新调度已调度的pod,这需要NodeLifecycleController驱逐无法容忍node污点的pod才能触发调度器重新调度。

本文引用源码为kubernetes的v1.24.2分支。

污点管理器

NoExecuteTaintManager

NoExecuteTaintManager是NoExecute污点管理器,用于管理具有NoExecute污点的node,并驱逐无法容忍这些污点的pod。NoExecuteTaintManager的定义参看源码链接:kubernetes/taint_manager.go at v1.24.2 · kubernetes/kubernetes · GitHub

type NoExecuteTaintManager struct {
    // 需要clientset一般有两个目的:
    // 1. 更新资源,比如Update、Patch、Delete等,很多controller在调和过程中需要更新资源;
    // 2. 获取比cache中更新版本的资源,因为clientset的Get()接口需要访问apiserver;
    // 遗憾的是,NoExecuteTaintManager没有使用clientset
    client                clientset.Interface
    // 笔者的习惯是Event相关的内容不做解释,后文所有关于event相关的代码笔者都不会注释
    recorder              record.EventRecorder
    // 下面三个函数是NodeLifecycleController注入的函数:
    // getPod:从cache获取Pod
    // getNode:从cache获取Node
    // getPodsAssignedToNode:从cache中获取调度到指定node的pods
    // 注入函数在单元测试时非常好用,因为可以注入预设的函数来判断函数执行结果是否符合预期
    getPod                GetPodFunc
    getNode               GetNodeFunc
    getPodsAssignedToNode GetPodsByNodeNameFunc

    // taintEvictionQueue是驱逐pod的队列,队列中都是等待驱逐的pod.
    // 驱逐pod的原因很简单,就是pod无法容忍NoExecute污点,就需要将其驱逐.
    // TimedWorkerQueue的实现非常简单,建议读者自行阅读代码,此处笔者只做一些简单的解释,便于后文的源码理解.
    // TimedWorkerQueue有Timed关键字说明有时间属性,这和pod容忍度中容忍时间有关,简单一句话就是过一段时间后再驱逐.
    // 所以taintEvictionQueue中都是等待容忍超时的pod,一旦容忍时间超时就会被立刻从node上驱逐.
    taintEvictionQueue *TimedWorkerQueue
    // taintedNodes的锁,因为有多个worker协程同时处理node的更新事件,存在并发访问taintedNodes的可能性
    taintedNodesLock sync.Mutex
    // taintedNodes记录了具有NoExecute污点的节点,key是节点的名字,value是NoExecute的污点.
    taintedNodes     map[string][]v1.Taint

    // 下面两个chan是NoExecuteTaintManager主协程与worker协程之间通信管道.
    // nodeUpdateChannels用于通知worker协程处理node更新事件.
    // podUpdateChannels用于通知worker协程处理pod更新事件.
    // 因为主协程需要与每个worker协程都要建立chan,所以nodeUpdateChannels和podUpdateChannels是slice类型.
    // 
    nodeUpdateChannels []chan nodeUpdateItem
    podUpdateChannels  []chan podUpdateItem

    // 下面两个队列用于存储node和pod的更新事件,NodeLifecycleController感知(watch)node和pod的更新事件,
    // 并通知给NoExecuteTaintManager,NoExecuteTaintManager将事件存储在相应的队列中,
    // 再通过nodeUpdateChannels和podUpdateChannels分发给worker协程处理。
    nodeUpdateQueue workqueue.Interface
    podUpdateQueue  workqueue.Interface
}

NewNoExecuteTaintManager

前一章节已经解析了NoExecuteTaintManager的定义,本章节对NoExecuteTaintManager的构造函数进行解析,源码链接:kubernetes/taint_manager.go at v1.24.2 · kubernetes/kubernetes · GitHub

// 构造NoExecuteTaintManager传入getPod、getNode、getPodsAssignedToNode函数,注入到NoExecuteTaintManager对象中。
func NewNoExecuteTaintManager(ctx context.Context, c clientset.Interface, getPod GetPodFunc, getNode GetNodeFunc, getPodsAssignedToNode GetPodsByNodeNameFunc) *NoExecuteTaintManager {
    eventBroadcaster := record.NewBroadcaster()
    recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "taint-controller"})
    eventBroadcaster.StartStructuredLogging(0)
    if c != nil {
        klog.V(0).InfoS("Sending events to api server")
        eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: c.CoreV1().Events("")})
    } else {
        klog.Fatalf("kubeClient is nil when starting NodeController")
    }

    // 创建NoExecuteTaintManager对象,感觉没啥可解释的
    tm := &NoExecuteTaintManager{
        client:                c,
        recorder:              recorder,
        getPod:                getPod,
        getNode:               getNode,
        getPodsAssignedToNode: getPodsAssignedToNode,
        taintedNodes:          make(map[string][]v1.Taint),

        nodeUpdateQueue: workqueue.NewNamed("noexec_taint_node"),
        podUpdateQueue:  workqueue.NewNamed("noexec_taint_pod"),
    }
    // 创建驱逐队列
    tm.taintEvictionQueue = CreateWorkerQueue(deletePodHandler(c, tm.emitPodDeletionEvent))

    return tm
}

Run

构造NoExecuteTaintManager对象后,就要让它运行起来处理node和pod的更新事件,接下来笔者详细的解析NoExecuteTaintManager是如何处理node和pod的更新事件,源码链接:https://github.com/kubernetes/kubernetes/blob/v1.24.2/pkg/controller/nodelifecycle/scheduler/taint_manager.go#L186

// Run()是NoExecuteTaintManager的主协程函数
func (tc *NoExecuteTaintManager) Run(ctx context.Context) {
    klog.V(0).InfoS("Starting NoExecuteTaintManager")

    // 为每个worker协程构造node和pod更新的消息管道,UpdateWorkerSize在代码中是常数8,
    // 说明NoExecuteTaintManager有8个worker协程处理node和pod的更新事件
    for i := 0; i < UpdateWorkerSize; i++ {
        tc.nodeUpdateChannels = append(tc.nodeUpdateChannels, make(chan nodeUpdateItem, NodeUpdateChannelSize))
        tc.podUpdateChannels = append(tc.podUpdateChannels, make(chan podUpdateItem, podUpdateChannelSize))
    }

    // 创建一个协程专门负责从nodeUpdateQueue读取更新的node然后再分发到nodeUpdateChannels中
    go func(stopCh <-chan struct{}) {
        for {
            // 从队列中读取一个更新的node
            item, shutdown := tc.nodeUpdateQueue.Get()
            if shutdown {
                break
            }
            // 根据node名字做哈希,就是根据节点名字计算一个int值,然后再取模
            // 这算是一种简单的负载均衡策略
            nodeUpdate := item.(nodeUpdateItem)
            hash := hash(nodeUpdate.nodeName, UpdateWorkerSize)
            // 再将更新node通过chan通知到worker协程,需要注意的是,此处可能会被阻塞,即便chan有buf.
            // 有没有发现这种负载均衡策略是有缺陷的,即可能出现某个worker协程很忙但是还会将更新事件派发给它.
            // 理论上出现这种情况的概率很低,因为chan有buf,但是不排除会发生.
            // 如果做到理想化的负载均衡,势必会增加计算开销,收益不见得比损失大.
            select {
            case <-stopCh:
                tc.nodeUpdateQueue.Done(item)
                return
            case tc.nodeUpdateChannels[hash] <- nodeUpdate:
                // tc.nodeUpdateQueue.Done is called by the nodeUpdateChannels worker
            }
        }
    }(ctx.Done())

    // 创建一个协程专门负责从podUpdateQueue读取更新的pod然后再分发到podUpdateChannels中
    // 因为方法与分发node是一样的,此处就不在多余注释了
    go func(stopCh <-chan struct{}) {
        for {
            item, shutdown := tc.podUpdateQueue.Get()
            if shutdown {
                break
            }
            podUpdate := item.(podUpdateItem)
            hash := hash(podUpdate.nodeName, UpdateWorkerSize)
            select {
            case <-stopCh:
                tc.podUpdateQueue.Done(item)
                return
            case tc.podUpdateChannels[hash] <- podUpdate:
                // tc.podUpdateQueue.Done is called by the podUpdateChannels worker
            }
        }
    }(ctx.Done())

    // 构造worker协程,并发的处理node和pod的更新事件
    // 此处需要注意:每个worker协程都会处理node和pod两种更新事件,而不是为node和pod的更新事件的某一种
    wg := sync.WaitGroup{}
    wg.Add(UpdateWorkerSize)
    for i := 0; i < UpdateWorkerSize; i++ {
        go tc.worker(ctx, i, wg.Done, ctx.Done())
    }

    // 等待所有的worker退出
    wg.Wait()
}

从代码上看,NoExecuteTaintManager的主协程主要职责就是pod和node更新事件的负载均衡。

worker

Run()将pod和node的更新事件通过chan派发到worker协程,本章节解析NoExecuteTaintManager的worker协程是如何处理这两个事件的,源码链接:https://github.com/kubernetes/kubernetes/blob/v1.24.2/pkg/controller/nodelifecycle/scheduler/taint_manager.go#L244

// worker()是NoExecuteTaintManager的worker协程函数
func (tc *NoExecuteTaintManager) worker(ctx context.Context, worker int, done func(), stopCh <-chan struct{}) {
    defer done()

    // NoExecuteTaintManager的worker协程就是从nodeUpdateChannels或者podUpdateChannels中获取相应的事件并处理它
    for {
        select {
        // 收到退出信号,退出worker协程
        case <-stopCh:
            return
        // 处理node更新事件
        case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
            // handleNodeUpdate()后文有注释,此处只需要知道用于处理node更新事件即可。
            tc.handleNodeUpdate(ctx, nodeUpdate)
            tc.nodeUpdateQueue.Done(nodeUpdate)
        // 处理pod更新事件
        case podUpdate := <-tc.podUpdateChannels[worker]:
            // 对于NoExecuteTaintManager来说,更感兴趣的是node的更新事件,因为node的污点更新可以立即触发相应的驱逐事件.
            // 说的直白点,此时node和pod的更新事件都来了,优先处理node事件可以立刻对已经调度到这些node上的pod产生作用,
            // 如果等处理完毕pod更新事件后再处理node更新事件,会造成已调度但未更新pod延迟驱逐,这应该不是我们希望看到的。
        priority:
            for {
                select {
                case nodeUpdate := <-tc.nodeUpdateChannels[worker]:
                    tc.handleNodeUpdate(ctx, nodeUpdate)
                    tc.nodeUpdateQueue.Done(nodeUpdate)
                default:
                    break priority
                }
            }
            // 当所有node的更新事件处理完后,再处理pod的更新事件,handlePodUpdate()后文有注释
            tc.handlePodUpdate(ctx, podUpdate)
            tc.podUpdateQueue.Done(podUpdate)
        }
    }
}

processPodOnNode

总感觉worker()函数没干啥,就是简单从chan中读取事件然后处理它,其中handlePodUpdate()和handleNodeUpdate()才是真正处理pod和node更新事件的地方。在解析这两个函数之前,需要知道processPodOnNode()函数,这个函数是理解前面两个函数的关键。源码链接:https://github.com/kubernetes/kubernetes/blob/v1.24.2/pkg/controller/nodelifecycle/scheduler/taint_manager.go#L340

// processPodOnNode()用于处理指定node上的某个pod,处理的方法就是根据pod的容忍度和node的污点决定是否驱逐pod
func (tc *NoExecuteTaintManager) processPodOnNode(
    ctx context.Context,
    podNamespacedName types.NamespacedName,
    nodeName string,
    tolerations []v1.Toleration,
    taints []v1.Taint,
    now time.Time,
) {
    // 如果node没有NoExecute污点,则取消pod的驱逐行为,这是node更新后删除所有NoExecute污点造成
    if len(taints) == 0 {
        // cancelWorkWithEvent()读者可以自行阅读源码,代码很少,也很简单,就如果pod在队列中等待被驱逐,则从队列中删除。
        tc.cancelWorkWithEvent(podNamespacedName)
    }
    // 获取匹配的容忍度,即pod的哪些容忍度对node的污点生效了
    allTolerated, usedTolerations := v1helper.GetMatchingTolerations(taints, tolerations)
    // 如果node有些污点pod无法容忍(即无法容忍全部污点)
    if !allTolerated {
        klog.V(2).InfoS("Not all taints are tolerated after update for pod on node", "pod", podNamespacedName.String(), "node", klog.KRef("", nodeName))
        // 如果pod还在队列中等待驱逐,则取消相应的工作,因为现在需要立刻驱逐.
        // 比如node新增了一些污点,原本还能容忍一段时间,现在就立刻无法容忍了.
        tc.cancelWorkWithEvent(podNamespacedName)
        // 将pod添加到驱逐队列,等待被驱逐,因为驱逐时间是现在,所以会立刻被驱逐.
        tc.taintEvictionQueue.AddWork(ctx, NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), time.Now(), time.Now())
        return
    }
    // 获取pod多个容忍度中最小容忍时间(TolerationSeconds),只对NoExecute污点生效。
    // 说的直白点,就是虽然能够容忍NoExecute的污点,但是有的容忍度是有时间限度的,超过指定的时间就容忍不了了。
    // 此处获取的是所有生效的容忍度中最小的容忍时间,但是需要特别注意的是,容忍时间小于等于0等同于无法容忍,需要立刻驱逐。
    minTolerationTime := getMinTolerationTime(usedTolerations)
    // minTolerationTime取值范围如下:
    // <0: 表示所有的容忍度都没有容忍时间,表示无限容忍
    // =0: 表示至少有一个容忍度的容忍时间为<=0,需要立刻驱逐
    // >0: 表示部分容忍度配置了>0的容忍时间,需要等到容忍时间超时后才能驱逐
    if minTolerationTime < 0 {
        // 无限容忍
        klog.V(4).InfoS("Current tolerations for pod tolerate forever, cancelling any scheduled deletion", "pod", podNamespacedName.String())
        tc.cancelWorkWithEvent(podNamespacedName)
        return
    }

    // 根据当前时间+容忍超时计算触发驱逐的时间
    startTime := now
    triggerTime := startTime.Add(minTolerationTime)
    // 如果pod已经被放入到驱逐队列,这需要根据pod或者node的更新决定是否需要重新放入驱逐队列,即驱逐时间更新了。
    scheduledEviction := tc.taintEvictionQueue.GetWorkerUnsafe(podNamespacedName.String())
    if scheduledEviction != nil {
        // 此处笔者实在看不懂,上一次放入驱逐队列的时间+容忍时间<当前时间+容忍时间,这个条件理论上永远成立
        // 最终的结果就是pod被放入驱逐队列后,即便pod的容忍时间有修改也无效。
        // 笔者猜作者也没想那么多,按照新的容忍时间重新放入驱逐队列可能会推迟驱逐,因为这个pod早就放入驱逐队列。
        // 笔者认为此处只需要校验容忍时间是否有变化即可:scheduledEviction.FireAt-scheduledEviction.CreatedAt!=minTolerationTime
        // 如果容忍时间发生了变化,则根据以前开始驱逐的时间计算新的驱逐时间,即triggerTime = startTime.Add(minTolerationTime)
        startTime = scheduledEviction.CreatedAt
        if startTime.Add(minTolerationTime).Before(triggerTime) {
            return
        }
        tc.cancelWorkWithEvent(podNamespacedName)
    }
    // 将pod放入驱逐队列,等待容忍时间超时后执行驱逐
    tc.taintEvictionQueue.AddWork(ctx, NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), startTime, triggerTime)
}

handlePodUpdate

从Run()函数到worker()函数,一直都在说NoExecuteTaintManager处理pod和node的更新事件,但是基本没看到任何处理事件的代码,都是事件的各种转发逻辑。本章节将真正解析NoExecuteTaintManager如何处理pod的更新事件,源码链接:https://github.com/kubernetes/kubernetes/blob/v1.24.2/pkg/controller/nodelifecycle/scheduler/taint_manager.go#L380

// handlePodUpdate()用于处理pod的更新事件
func (tc *NoExecuteTaintManager) handlePodUpdate(ctx context.Context, podUpdate podUpdateItem) {
    // 从cache中获取pod资源对象
    pod, err := tc.getPod(podUpdate.podName, podUpdate.podNamespace)
    if err != nil {
        if apierrors.IsNotFound(err) {
            // Delete
            podNamespacedName := types.NamespacedName{Namespace: podUpdate.podNamespace, Name: podUpdate.podName}
            klog.V(4).InfoS("Noticed pod deletion", "pod", podNamespacedName)
            tc.cancelWorkWithEvent(podNamespacedName)
            return
        }
        utilruntime.HandleError(fmt.Errorf("could not get pod %s/%s: %v", podUpdate.podName, podUpdate.podNamespace, err))
        return
    }

    // 校验pod更新事件发生时刻到现在是否又发生了变化,主要指的是调度到的node是否修改,如果有修改,忽略该更新事件即可.
    // 因为后面还有更新事件等待处理,新的事件因为node修改,需要回滚此次事件的所有内容,等同于不用处理此事件.
    if pod.Spec.NodeName != podUpdate.nodeName {
        return
    }

    podNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}
    klog.V(4).InfoS("Noticed pod update", "pod", podNamespacedName)
    nodeName := pod.Spec.NodeName
    if nodeName == "" {
        return
    }
    // 获取pod调度到node的污点(NoExecute)信息
    taints, ok := func() ([]v1.Taint, bool) {
        tc.taintedNodesLock.Lock()
        defer tc.taintedNodesLock.Unlock()
        taints, ok := tc.taintedNodes[nodeName]
        return taints, ok
    }()
    // 如果pod调度的node没有污点,则不需要处理
    if !ok {
        return
    }
    // 如果node有污点,则根据pod的容忍度决定是否驱逐
    tc.processPodOnNode(ctx, podNamespacedName, nodeName, pod.Spec.Tolerations, taints, time.Now())
}

是不是感觉处理pod的更新事件有点简单?其实很多事情就是没有想象的那么复杂,对于NoExecuteTaintManager来说,只要判断pod更新后是否能够容忍node的污点,如果不能就驱逐它,就是这么简单。

handleNodeUpdate

了解了NoExecuteTaintManager如何处理pod更新事件,那么就很容易猜到如何处理node的更新事件:判断node的污点更新后已经调度到该node上的pod是否能够容忍,不能容忍的就驱逐掉。源码链接:https://github.com/kubernetes/kubernetes/blob/v1.24.2/pkg/controller/nodelifecycle/scheduler/taint_manager.go#L420

// handleNodeUpdate()是处理node更新事件的函数
func (tc *NoExecuteTaintManager) handleNodeUpdate(ctx context.Context, nodeUpdate nodeUpdateItem) {
    // 通过node名字获取node信息
    node, err := tc.getNode(nodeUpdate.nodeName)
    if err != nil {
        if apierrors.IsNotFound(err) {
            // Delete
            klog.V(4).InfoS("Noticed node deletion", "node", nodeUpdate.nodeName)
            tc.taintedNodesLock.Lock()
            defer tc.taintedNodesLock.Unlock()
            delete(tc.taintedNodes, nodeUpdate.nodeName)
            return
        }
        utilruntime.HandleError(fmt.Errorf("cannot get node %s: %v", nodeUpdate.nodeName, err))
        return
    }

    // 获取node更新后的污点信息,需要注意,此处只关注NoExecute的污点,具体原因前面已经解释过了。
    klog.V(4).InfoS("Noticed node update", "node", nodeUpdate)
    taints := getNoExecuteTaints(node.Spec.Taints)
    // 根据node更新后是否具有NoExecute污点决定是否将node放入污点node集合
    func() {
        tc.taintedNodesLock.Lock()
        defer tc.taintedNodesLock.Unlock()
        klog.V(4).InfoS("Updating known taints on node", "node", node.Name, "taints", taints)
        if len(taints) == 0 {
            delete(tc.taintedNodes, node.Name)
        } else {
            tc.taintedNodes[node.Name] = taints
        }
    }()

    // 获取调度到该node的所有pod
    pods, err := tc.getPodsAssignedToNode(node.Name)
    if err != nil {
        klog.ErrorS(err, "Failed to get pods assigned to node", "node", node.Name)
        return
    }
    if len(pods) == 0 {
        return
    }

    // 如果node更新后没有NoExecute污点,则取消所有等待驱逐的pod任务
    // 这说明此次node更新删除了所有NoExecute污点,抑或就一直没有NoExecute污点
    if len(taints) == 0 {
        klog.V(4).InfoS("All taints were removed from the node. Cancelling all evictions...", "node", node.Name)
        for i := range pods {
            tc.cancelWorkWithEvent(types.NamespacedName{Namespace: pods[i].Namespace, Name: pods[i].Name})
        }
        return
    }

    // node有NoExecute污点,则驱逐所有无法容忍更新后污点的pod,当然有些pod可以容忍更新后的污点也会取消驱逐.
    now := time.Now()
    for _, pod := range pods {
        podNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}
        tc.processPodOnNode(ctx, podNamespacedName, node.Name, pod.Spec.Tolerations, taints, now)
    }
}

从上面的代码可以看出优先处理node更新事件的必要性,更新后的污点会立刻作用在调度到该node上的pod。如果因为pod更新事件推迟了处理node更新事件,对于整个系统的收益并不高。

deletePodHandler

无论是pod还是node的更新事件,NoExecuteTaintManager处理这些事件的方式是唯一的,就是驱逐无法容忍node污点的pod。那么NoExecuteTaintManager是如何驱逐pod的呢?源码链接:kubernetes/taint_manager.go at v1.24.2 · kubernetes/kubernetes · GitHub

// deletePodHandler()返回驱逐pod的函数,从函数名可以看出驱逐就是删除,是不是有点简单粗暴了?
func deletePodHandler(c clientset.Interface, emitEventFunc func(types.NamespacedName)) func(ctx context.Context, args *WorkArgs) error {
    return func(ctx context.Context, args *WorkArgs) error {
        ns := args.NamespacedName.Namespace
        name := args.NamespacedName.Name
        klog.V(0).InfoS("NoExecuteTaintManager is deleting pod", "pod", args.NamespacedName.String())
        // 在驱逐pod之前先创建event,通知大家pod因为污点的被驱逐了
        if emitEventFunc != nil {
            emitEventFunc(args.NamespacedName)
        }
        // 驱逐pod的方法很简单,就是删除pod。删除可能会失败,此处做了多次(5次)尝试
        var err error
        for i := 0; i < retries; i++ {
            err = c.CoreV1().Pods(ns).Delete(ctx, name, metav1.DeleteOptions{})
            if err == nil {
                break
            }
            time.Sleep(10 * time.Millisecond)
        }
        return err
    }
}

PodUpdated

前面的章节已经解析了NoExecuteTaintManager是如何处理pod和node的更新事件的,那么NoExecuteTaintManager是如何感知这些事件的呢?熟悉informer的读者第一意识就是informer,没错,就是informer!但是是NodeLifecycleController通过informer注册了处理事件的函数,NodeLifecycleController将事件同步给NoExecuteTaintManager。PodUpdated()就是NodeLifecycleController同步pod更新事件给NoExecuteTaintManager的接口,源码链接:kubernetes/taint_manager.go at v1.24.2 · kubernetes/kubernetes · GitHub

// PodUpdated()是NoExecuteTaintManager处理pod更新事件的接口,NoExecuteTaintManager并不感知pod更新事件,
// 需要其他对象(比如NodeLifecycleController)调用该接口同步事件
func (tc *NoExecuteTaintManager) PodUpdated(oldPod *v1.Pod, newPod *v1.Pod) {
    // 获取的pod的名字,命名空间,调度到node名字以及旧的污点容忍度
    podName := ""
    podNamespace := ""
    nodeName := ""
    oldTolerations := []v1.Toleration{}
    if oldPod != nil {
        podName = oldPod.Name
        podNamespace = oldPod.Namespace
        nodeName = oldPod.Spec.NodeName
        oldTolerations = oldPod.Spec.Tolerations
    }
    // 获取pod新的污点容忍度
    newTolerations := []v1.Toleration{}
    if newPod != nil {
        podName = newPod.Name
        podNamespace = newPod.Namespace
        nodeName = newPod.Spec.NodeName
        newTolerations = newPod.Spec.Tolerations
    }

    // 如果pod调度节点没有发生变化并且污点容忍度也没有发生变化,则忽略此事件,因为此次pod更新不会造成pod驱逐
    if oldPod != nil && newPod != nil && helper.Semantic.DeepEqual(oldTolerations, newTolerations) && oldPod.Spec.NodeName == newPod.Spec.NodeName {
        return
    }
    // 添加到pod更新队列等待worker协程处理该事件,进而触发前面一大堆处理pod更新事件的流程
    updateItem := podUpdateItem{
        podName:      podName,
        podNamespace: podNamespace,
        nodeName:     nodeName,
    }

    tc.podUpdateQueue.Add(updateItem)
}

NodeUpdated

啥也别说了,直接上代码吧,源码链接:kubernetes/taint_manager.go at v1.24.2 · kubernetes/kubernetes · GitHub

// NodeUpdated()是NoExecuteTaintManager处理node更新事件的接口,NoExecuteTaintManager并不感知node更新事件,
// 需要其他对象(比如NodeLifecycleController)调用该接口同步事件
func (tc *NoExecuteTaintManager) NodeUpdated(oldNode *v1.Node, newNode *v1.Node) {
    // 获取节点名和旧的污点
    nodeName := ""
    oldTaints := []v1.Taint{}
    if oldNode != nil {
        nodeName = oldNode.Name
        oldTaints = getNoExecuteTaints(oldNode.Spec.Taints)
    }
    // 获取新污点
    newTaints := []v1.Taint{}
    if newNode != nil {
        nodeName = newNode.Name
        newTaints = getNoExecuteTaints(newNode.Spec.Taints)
    }
    // 如果node污点没有发生变化则忽略该node更新事件
    if oldNode != nil && newNode != nil && helper.Semantic.DeepEqual(oldTaints, newTaints) {
        return
    }
    // 将node放入队列等待worker协程处理node更新事件,进而触发前面一大堆处理node更新事件的流程
    updateItem := nodeUpdateItem{
        nodeName: nodeName,
    }

    tc.nodeUpdateQueue.Add(updateItem)
}

总结

  1. NoExecuteTaintManager的核心职责就是驱逐无法容忍node污点(NoExecute)的pod,如上图所示终点是deletePodHandler;
  2. NoExecuteTaintManager通过感知pod和node的更新事件并及时处理来达到较高的时效性;
  3. NoExecuteTaintManager只会对pod的容忍度(NoExecute)和node污点(NoExecute)的更新感兴趣,因为只有容忍度和污点的更新才可能引发驱逐,当然pod调度到的node变化也需要被关注;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值