概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob UpdateFuncPod AddFuncPod UpdateFuncPod DeleteFunc

概述

源码版本:kubernetes master 分支 commit-fe62fc(2021年10月14日)

Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernetes 之上运行批处理任务最简单的方式,在 AI 模型训练等场景下最基础的实现版本就是拉起一个 Job 来完成一次训练任务,然后才是各种自定义 “Job” 实现进阶处理,比如分布式训练需要一个 “Job” 同时拉起多个 Pod,但是每个 Pod 的启动参数会有差异。所以深入理解 Job 的功能和实现细节是进一步开发自定义 “Job” 类型工作负载的基础。

我们在《Kubernetes Job Controller 原理和源码分析(一)》中详细介绍了 Job 的特性,今天我们继续从源码角度剖析下 Job 的实现。

注意:阅读 Job 源码需要有一定的自定义控制器工作原理基础,里面涉及到了 Informer 工作机制、workqueue(延时工作队列)、ResourceEventHandler 等等逻辑,没有相关知识储备直接看本文会有一定挑战,建议先阅读《深入理解 K8S 原理与实现》系列目录里列的相关文章。


《Kubernetes Job Controller 原理和源码分析》分为三讲:

程序入口

Job 控制器代码入口在 pkg/controller/job 包的 job_controller.go 源文件里。在这个源文件里可以看到一个 NewController() 函数,用于新建一个 Job controller,这个 controller 也就是用来调谐 job 对象和其对应的 pods 的控制器。另外 Controller 对象有一个 Run() 方法,用于启动这个控制器的主流程,开始 watch 和 sync 所有的 job 对象。

这里的组织方式还是很清晰,一个 NewXxx() 函数配合一个 Run() 方法,完成一个对象的初始化和启动流程。如果向上再跟一级 Run() 方法的调用入口,我们还可以看到 cmd 里有这样一段代码:

  • cmd/kube-controller-manager/app/batch.go:34
1func startJobController(ctx context.Context, controllerContext ControllerContext) (controller.Interface, bool, error) {
2    go job.NewController(
3        controllerContext.InformerFactory.Core().V1().Pods(),
4        controllerContext.InformerFactory.Batch().V1().Jobs(),
5        controllerContext.ClientBuilder.ClientOrDie("job-controller"),
6    ).Run(int(controllerContext.ComponentConfig.JobController.ConcurrentJobSyncs), ctx.Done())
7    return nil, true, nil
8}

这里的逻辑是调用 job.NewController() 方法获取一个 Job controller 然后调用其 Run() 方法来完成控制器启动逻辑,前者的三个参数分别是 podInformer、jobInformer 和 kubeClient,后者的参数主要是并发数,也就是同时开启几个调谐 loop。

Job controller 的创建

Controller 对象

回到 NewController() 函数,我们看下里面的主要逻辑。函数声明是这样的:

1func NewController(podInformer coreinformers.PodInformer, jobInformer batchinformers.JobInformer, kubeClient clientset.Interface) *Controller
2

参数有三个:

  • podInformer coreinformers.PodInformer
  • jobInformer batchinformers.JobInformer
  • kubeClient clientset.Interface

这里的 PodInformer 和 JobInformer 引用在初始化 Job controller 的时候用来设置 EventHandlerFunc,另外设置 Job controller 的 jobLister 和 podStore 属性;kubeClient 是 clientset.Interface 类型,也就是可以用来分别 Get Job 和 Pod,后面会讲到。

返回值是 *Controller 类型,至于为什么不叫做 JobController,咱也不知道,咱也不敢问,反正 DeploymentController、ReplicaSetController 等命名都是带上类型的,这里多多少少给人感觉不清晰。不同开发者的风格吧。先看一下类型定义:

  • pkg/controller/job/job_controller.go:80
 1type Controller struct {
2   kubeClient clientset.Interface
3   podControl controller.PodControlInterface
4   updateStatusHandler func(job *batch.Job) error
5   patchJobHandler     func(job *batch.Job, patch []byte) error
6   syncHandler         func(jobKey string) (bool, error)
7   podStoreSynced cache.InformerSynced // pod 存储是否至少更新过一次
8   jobStoreSynced cache.InformerSynced // job 存储是否至少更新过一次
9   expectations controller.ControllerExpectationsInterface
10   jobLister batchv1listers.JobLister // jobs 存储
11   podStore corelisters.PodLister // pods 存储
12   queue workqueue.RateLimitingInterface // 需要更新的 Job 队列
13   orphanQueue workqueue.RateLimitingInterface // 孤儿 pods 队列,用来给 Job 追踪 finalizer 执行删除
14   recorder record.EventRecorder // 用于发送 Event
15}
16

后面在具体的函数中看下上面的属性都有些啥作用。

NewController()

这里有一堆的方法调用,我们先看下整体流程:

 1func NewController(podInformer coreinformers.PodInformer, jobInformer batchinformers.JobInformer, kubeClient clientset.Interface) *Controller {
2   // event 初始化,我们在《Kubernetes Event 原理和源码分析》中详细介绍过 event 的逻辑
3   eventBroadcaster := record.NewBroadcaster()
4   eventBroadcaster.StartStructuredLogging(0)
5   eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
6
7   // ……
8
9   // 初始化 Controller,jm 应该是 job manager 的意思
10   jm := &Controller{
11      kubeClient: kubeClient,
12      podControl: controller.RealPodControl{
13         KubeClient: kubeClient,
14         Recorder:   eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "job-controller"}),
15      },
16      expectations: controller.NewControllerExpectations(),
17      // 限速队列我们在《Kubernetes client-go 源码分析 - workqueue》中详细介绍过
18      queue:        workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(DefaultJobBackOff, MaxJobBackOff), "job"),
19      orphanQueue:  workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(DefaultJobBackOff, MaxJobBackOff), "job_orphan_pod"),
20      recorder:     eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "job-controller"}),
21   }
22
23   // 配置 jobInformer 的 ResourceEventHandler
24   jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
25      AddFunc: func(obj interface{}) {
26         jm.enqueueController(obj, true) // 增
27      },
28      UpdateFunc: jm.updateJob, // 改
29      DeleteFunc: func(obj interface{}) {
30         jm.enqueueController(obj, true) // 删
31      },
32   })
33   jm.jobLister = jobInformer.Lister()
34   jm.jobStoreSynced = jobInformer.Informer().HasSynced
35
36   // 配置 jobInformer 的 ResourceEventHandler
37   podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
38      AddFunc:    jm.addPod,
39      UpdateFunc: jm.updatePod,
40      DeleteFunc: jm.deletePod,
41   })
42   jm.podStore = podInformer.Lister()
43   jm.podStoreSynced = podInformer.Informer().HasSynced
44
45   jm.updateStatusHandler = jm.updateJobStatus
46   jm.patchJobHandler = jm.patchJob
47   jm.syncHandler = jm.syncJob
48
49   metrics.Register()
50
51   return jm
52}

这里有可以看到 EventHandler 的逻辑入口,下面会具体分析,我们先看 New 函数里的 podControl,看下具体定义:

podControl

这个属性的定义如下

1podControl controller.PodControlInterface

PodControlInterface 是一个接口类型,定义如下:

1type PodControlInterface interface {
2   CreatePods(namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference) error
3   CreatePodsWithGenerateName(namespace string, template *v1.PodTemplateSpec, object runtime.Object, controllerRef *metav1.OwnerReference, generateName string) error
4   DeletePod(namespace string, podID string, object runtime.Object) error
5   PatchPod(namespace, name string, data []byte) error
6}

这个接口就是用来创建、删除 pod 的。前面实例化的时候传递的是:

1controller.RealPodControl{
2   KubeClient: kubeClient,
3   Recorder:   eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "job-controller"}),
4},

RealPodControl 是一个结构体,具体实现了上述 PodControlInterface 接口。

EventHandler

Job AddFunc DeleteFunc

前面我们看到这段代码:

1    jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
2        AddFunc: func(obj interface{}) {
3            jm.enqueueController(obj, true)
4        },
5        UpdateFunc: jm.updateJob,
6        DeleteFunc: func(obj interface{}) {
7            jm.enqueueController(obj, true)
8        },
9    })

可以看到在 Job 类型资源对象实例新增和删除的时候,都是执行的 jm.enqueueController(obj, true),我们继续看这个方法的逻辑:

  • pkg/controller/job/job_controller.go:417
1func (jm *Controller) enqueueController(obj interface{}, immediate bool) {   // 计算 key   key, err := controller.KeyFunc(obj)   if err != nil {      utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %+v: %v", obj, err))      return   }   // 如果指定了 immediate 就马上重试   backoff := time.Duration(0)   if !immediate {      // 这里的重试间隔是用的 10s 乘以 2 的 n-1 次方,n 是 requeue 次数,也就是这个对象 requeue 到 workqueue 的次数      backoff = getBackoff(jm.queue, key)   }   klog.Infof("enqueueing job %s", key)   // 加入到 workqueue,这是一个延时队列   jm.queue.AddAfter(key, backoff)}

Job UpdateFunc

继续看 Job 类型资源对象实例更新到时候代码逻辑,这里相比 AddFunc 主要多了一个 ActiveDeadlineSeconds 的处理逻辑:

1func (jm *Controller) updateJob(old, cur interface{}) {   oldJob := old.(*batch.Job)   curJob := cur.(*batch.Job)   key, err := controller.KeyFunc(curJob)   if err != nil {      return   }   // 这里和 AddFunc 逻辑一样,新对象加入到 workqueue   jm.enqueueController(curJob, true)   // check if need to add a new rsync for ActiveDeadlineSeconds   if curJob.Status.StartTime != nil {      curADS := curJob.Spec.ActiveDeadlineSeconds      // 检查新对象是否配置 ActiveDeadlineSeconds,没有则直接返回      if curADS == nil {         return      }      // 到这里说明新对象配置了 ActiveDeadlineSeconds      oldADS := oldJob.Spec.ActiveDeadlineSeconds      // 下面逻辑是计算 ActiveDeadlineSeconds 配置的时间 - 当前已经使用的时间 得到一个剩余到期时间,然后将这个延时写入延时队列,从而实现在 ActiveDeadlineSeconds 到期时调谐流程可以被触发      if oldADS == nil || *oldADS != *curADS {         now := metav1.Now()         start := curJob.Status.StartTime.Time         passed := now.Time.Sub(start)         total := time.Duration(*curADS) * time.Second         // AddAfter will handle total < passed         jm.queue.AddAfter(key, total-passed)         klog.V(4).Infof("job %q ActiveDeadlineSeconds updated, will rsync after %d seconds", key, total-passed)      }   }}

Pod AddFunc

继续看 Pod 新增时对应到操作,我们在前面看到过这几行代码:

1podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
2        AddFunc:    jm.addPod,
3        UpdateFunc: jm.updatePod,
4        DeleteFunc: func(obj interface{}) {
5            jm.deletePod(obj, true)
6        },
7    })

这里可以看到 Pod 增删改时分别对应的代码处理入口,先来看 add 动作:

  • pkg/controller/job/job_controller.go:232
 1func (jm *Controller) addPod(obj interface{}) {
2   pod := obj.(*v1.Pod)
3   if pod.DeletionTimestamp != nil {
4      // 当控制器重启的时候可以会收到一个处于删除状态的 Pod 新增事件
5      jm.deletePod(pod, false)
6      return
7   }
8
9   if controllerRef := metav1.GetControllerOf(pod); controllerRef != nil {
10      // 查询这个 pod 归哪个 job 管
11      job := jm.resolveControllerRef(pod.Namespace, controllerRef)
12      if job == nil {
13         return
14      }
15      jobKey, err := controller.KeyFunc(job)
16      if err != nil {
17         return
18      }
19      // 记录动作
20      jm.expectations.CreationObserved(jobKey)
21      // 将发生 pod 新增事件对应的 job 加入到 workqueue
22      jm.enqueueController(job, true)
23      return
24   }
25
26   // 如果没有 controllerRef 配置,那么这是一个 孤儿 pod,这时候还是通知对应的 job 来认领这个 pod
27   for _, job := range jm.getPodJobs(pod) {
28      jm.enqueueController(job, true)
29   }
30}

Pod UpdateFunc

继续来看 Pod 对象的更新动作:

 1func (jm *Controller) updatePod(old, cur interface{}) {
2   curPod := cur.(*v1.Pod)
3   oldPod := old.(*v1.Pod)
4   if curPod.ResourceVersion == oldPod.ResourceVersion {
5      // 周期性的 resync 会触发 Update 事件,通过比较 RV 可以过滤一些不必要的操作
6      return
7   }
8   if curPod.DeletionTimestamp != nil {
9      // 如果是正在被删除的 Pod
10      jm.deletePod(curPod, false)
11      return
12   }
13
14   // 如果 Pod 已经跪了,immediate 就为 false
15   immediate := curPod.Status.Phase != v1.PodFailed
16
17   // 新 pod 是否没有 finalizer 配置
18   finalizerRemoved := !hasJobTrackingFinalizer(curPod)
19   curControllerRef := metav1.GetControllerOf(curPod)
20   oldControllerRef := metav1.GetControllerOf(oldPod)
21   // 判断新旧 Pod 是否属于同一个控制器管理
22   controllerRefChanged := !reflect.DeepEqual(curControllerRef, oldControllerRef)
23   if controllerRefChanged && oldControllerRef != nil {
24      // 如果 ControllerRef 已经变了,这时候需要通知旧的控制器
25      if job := jm.resolveControllerRef(oldPod.Namespace, oldControllerRef); job != nil {
26         if finalizerRemoved {
27            key, err := controller.KeyFunc(job)
28            if err == nil {
29               jm.finalizerExpectations.finalizerRemovalObserved(key, string(curPod.UID))
30            }
31         }
32         jm.enqueueController(job, immediate)
33      }
34   }
35
36   // 如果 ControllerRef 一致
37   if curControllerRef != nil {
38      // 提取这个 pod 对应的所属 job
39      job := jm.resolveControllerRef(curPod.Namespace, curControllerRef)
40      if job == nil {
41         return
42      }
43      if finalizerRemoved {
44         key, err := controller.KeyFunc(job)
45         if err == nil {
46            jm.finalizerExpectations.finalizerRemovalObserved(key, string(curPod.UID))
47         }
48      }
49      // 加入到 workqueue
50      jm.enqueueController(job, immediate)
51      return
52   }
53
54   // 代码如果执行到这里,说明这是一个孤儿 pod,这时候尝试寻找一个能够认领的 job,如果有则通知其认领
55   labelChanged := !reflect.DeepEqual(curPod.Labels, oldPod.Labels)
56   if labelChanged || controllerRefChanged {
57      for _, job := range jm.getPodJobs(curPod) {
58         jm.enqueueController(job, immediate)
59      }
60   }
61}

Pod DeleteFunc

最后看下 Pod 删除时的操作

 1func (jm *Controller) deletePod(obj interface{}, final bool) {
2   pod, ok := obj.(*v1.Pod)
3
4   // 处理 DeletedFinalStateUnknown 场景
5   if !ok {
6      tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
7      if !ok {
8         utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %+v", obj))
9         return
10      }
11      pod, ok = tombstone.Obj.(*v1.Pod)
12      if !ok {
13         utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a pod %+v", obj))
14         return
15      }
16   }
17   // 和前面类似,查询这个 pod 归哪个 job 管
18   controllerRef := metav1.GetControllerOf(pod)
19   if controllerRef == nil {
20      // No controller should care about orphans being deleted.
21      return
22   }
23   job := jm.resolveControllerRef(pod.Namespace, controllerRef)
24   if job == nil {
25      if hasJobTrackingFinalizer(pod) {
26         jm.enqueueOrphanPod(pod)
27      }
28      return
29   }
30   jobKey, err := controller.KeyFunc(job)
31   if err != nil {
32      return
33   }
34   jm.expectations.DeletionObserved(jobKey)
35
36   if final || !hasJobTrackingFinalizer(pod) {
37      jm.finalizerExpectations.finalizerRemovalObserved(jobKey, string(pod.UID))
38   }
39   // 加入 workqueue
40   jm.enqueueController(job, true)
41}

(转载请保留本文原始链接 https://www.danielhu.cn)

Kubernetes Job Controller 原理和源码分析(二)的更多相关文章

  1. Kubernetes Job Controller 原理和源码分析(一)

    概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...

  2. Kubernetes Job Controller 原理和源码分析(三)

    概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...

  3. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  4. Java1.7 HashMap 实现原理和源码分析

    HashMap 源码分析是面试中常考的一项,下面一篇文章讲得很好,特地转载过来. 本文转自:https://www.cnblogs.com/chengxiao/p/6059914.html 参考博客: ...

  5. ☕【Java深层系列】「并发编程系列」让我们一起探索一下CyclicBarrier的技术原理和源码分析

    CyclicBarrier和CountDownLatch CyclicBarrier和CountDownLatch 都位于java.util.concurrent这个包下,其工作原理的核心要点: Cy ...

  6. 深入ReentrantLock的实现原理和源码分析

    ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...

  7. Alibaba-技术专区-RocketMQ 延迟消息实现原理和源码分析

    痛点背景 业务场景 假设有这么一个需求,用户下单后如果30分钟未支付,则该订单需要被关闭.你会怎么做? 之前方案 最简单的做法,可以服务端启动个定时器,隔个几秒扫描数据库中待支付的订单,如果(当前时间 ...

  8. Express工作原理和源码分析一:创建路由

    Express是一基于Node的一个框架,用来快速创建Web服务的一个工具,为什么要使用Express呢,因为创建Web服务如果从Node开始有很多繁琐的工作要做,而Express为你解放了很多工作, ...

  9. Android AsyncTask运作原理和源码分析

    自10年大量看源码后,很少看了,抽时间把最新的源码看看! public abstract class AsyncTask<Params, Progress, Result> {     p ...

随机推荐

  1. SQList基础+ListView基本使用

    今日所学: SQList基础语法 SDList下载地址 SQLite Download Page SQList安装教程SQLite的安装与基本操作 - 极客开发者-博客 ListView用法 没遇到什 ...

  2. Array.fill()函数的用法

    ES6,Array.fill()函数的用法   ES6为Array增加了fill()函数,使用制定的元素填充数组,其实就是用默认内容初始化数组. 该函数有三个参数. arr.fill(value, s ...

  3. Struct2中三种获取表单数据的方式

    1.使用ActionContext类 //1获取ActionContext对象 ActionContext context = ActionContext.getContext(); //2.调用方法 ...

  4. mysql server_id的用途(主从等结构中)

    前言 我们都知道MySQL用server-id来唯一的标识某个数据库实例,并在链式或双主复制结构中用它来避免sql语句的无限循环.5.7需要同时设置server_id参数,8.0开始server_id ...

  5. OpenHarmony 3.1 Beta版本关键特性解析——探秘隐式查询

    ​(以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点)​ 徐浩 隐式查询是 OpenAtom OpenHarmony(以下简称"OpenHarmony" ...

  6. 【2021 ICPC Asia Jinan 区域赛】 C Optimal Strategy推公式-组合数-逆元快速幂

    题目链接 题目详情 (pintia.cn) 题目 题意 有n个物品在他们面前,编号从1自n.两人轮流移走物品.在移动中,玩家选择未被拿走的物品并将其拿走.当所有物品被拿走时,游戏就结束了.任何一个玩家 ...

  7. thinkphp6事件监听event-listene

    事件系统可以看成是行为系统的升级版,相比行为系统强大的地方在于事件本身可以是一个类,并且可以更好的支持事件订阅者. 事件相比较中间件的优势是事件比中间件更加精准定位(或者说粒度更细),并且更适合一些业 ...

  8. 标准输入输出() & 打印流 &配置文件

    public static void main(String[] args) { //System 类 的 public final static InputStream in = null; // ...

  9. VMware配置与管理DNS服务器

    一,安装DNS服务器角色 1,点击[开始]→[管理工具]→[服务器管理器]→"仪表板"选项的[添加角色和功能] 持续单击[下一步],直到出现"选择服务器角色"窗 ...

  10. IDEA新建项目时的默认配置与模版配置

    今天一大早,群里(点击加群)有小伙伴问了这样的一个问题: 在我们使用IDEA开发项目的时候,通常都会有很多配置项需要去设置,比如对于Java项目来说,一般就包含:JDK配置.Maven配置等.那么如果 ...