Kubernetes Job Controller 原理和源码分析(二)
概述程序入口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 原理和源码分析》分为三讲:
- 《Kubernetes Job Controller 原理和源码分析(一)》 - 详细介绍 Job 的用法和支持的特性
- 《Kubernetes Job Controller 原理和源码分析(二)》 - 源码分析第一部分,从控制器入口一直到所有 EventHandler 的具体实现,也就是“调谐任务”进入 workqueue 之前的全部逻辑
- 《Kubernetes Job Controller 原理和源码分析(三)》 - 源码分析第二部分,从 workqueue 消费“调谐任务”,具体的调谐过程实现等代码逻辑
程序入口
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 原理和源码分析(二)的更多相关文章
- Kubernetes Job Controller 原理和源码分析(一)
概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...
- Kubernetes Job Controller 原理和源码分析(三)
概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...
- Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析
相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...
- Java1.7 HashMap 实现原理和源码分析
HashMap 源码分析是面试中常考的一项,下面一篇文章讲得很好,特地转载过来. 本文转自:https://www.cnblogs.com/chengxiao/p/6059914.html 参考博客: ...
- ☕【Java深层系列】「并发编程系列」让我们一起探索一下CyclicBarrier的技术原理和源码分析
CyclicBarrier和CountDownLatch CyclicBarrier和CountDownLatch 都位于java.util.concurrent这个包下,其工作原理的核心要点: Cy ...
- 深入ReentrantLock的实现原理和源码分析
ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...
- Alibaba-技术专区-RocketMQ 延迟消息实现原理和源码分析
痛点背景 业务场景 假设有这么一个需求,用户下单后如果30分钟未支付,则该订单需要被关闭.你会怎么做? 之前方案 最简单的做法,可以服务端启动个定时器,隔个几秒扫描数据库中待支付的订单,如果(当前时间 ...
- Express工作原理和源码分析一:创建路由
Express是一基于Node的一个框架,用来快速创建Web服务的一个工具,为什么要使用Express呢,因为创建Web服务如果从Node开始有很多繁琐的工作要做,而Express为你解放了很多工作, ...
- Android AsyncTask运作原理和源码分析
自10年大量看源码后,很少看了,抽时间把最新的源码看看! public abstract class AsyncTask<Params, Progress, Result> { p ...
随机推荐
- Hadoop 3.1.2报错:xception in thread "main" org.apache.hadoop.fs.UnsupportedFileSystemException: No FileSystem for scheme "hdfs"
报错内容如下: Exception in thread "main" org.apache.hadoop.fs.UnsupportedFileSystemException: No ...
- 实际业务处理 Kafka 消息丢失、重复消费和顺序消费的问题
关于 Kafka 消息丢失.重复消费和顺序消费的问题 消息丢失,消息重复消费,消息顺序消费等问题是我们使用 MQ 时不得不考虑的一个问题,下面我结合实际的业务来和你分享一下解决方案. 消息丢失问题 比 ...
- 使用 NIO 搭建一个聊天室
使用 NIO 搭建一个聊天室 前面刚讲了使用 Socket 搭建了一个 Http Server,在最后我们使用了 NIO 对 Server 进行了优化,然后有小伙伴问到怎么使用 Socket 搭建聊天 ...
- LC-209
给定一个含有 n 个正整数的数组和一个正整数 target . 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, nums ...
- 聊聊UI自动化的PageObject设计模式
当我们开发UI自动化测试用例时,需要引用页面中的元素(数据)才能够进行点击(动作)并显示出页面内容.如果我们开发的用例是直接对HTML元素进行操作,则这样的用例无法"应对"页面中U ...
- ArcGIS使用技巧(四)——山体阴影
新手,若有错误还请指正! 最近在制图的时候出现如下的情况(图1),怎么调整Display的三个参数都没用. 图 1 查看其信息,发现dem的像元大小为0.00027(图2),是未投影的 图 2 查看A ...
- IP协议/地址(IPv4&IPv6)概要
IP协议/地址(IPv4&IPv6)概要 IP协议 什么是IP协议 IP是Internet Protocol(网际互连协议)的缩写,是TCP/IP体系中的网络层协议. [1] 协议的特征 无连 ...
- docker基础_网络模式
docker网络 网络模式: bridge:docker默认 自己创建会默认使用bridge模式 类似vmware中的NAT模式 其中192.168.1.203是本机在现实世界局域网的ip.172.1 ...
- 开源框架YiShaAdmin如何使用任务计划
1.在Startup添加 new JobCenter().Start();(红色字体,下同) // This method gets called by the runtime. Use this m ...
- zookeeper篇-zk的选举机制
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 说说zk的选举机制 基础概念 zxid=事务id=一个时间戳,代表当前事 ...