背景
k8s能够帮助我们的服务实现服务高可用,其提供的副本机制能够有效的保证运行实例的副本数,从而当某个实例异常后服务可以重新被自动唤起,但在我们的生产环境中,某些特殊的服务(如广告资金服务或计费服务)因服务重启期间而导致的业务中断,对业务请求的延时响应也是不可忽略的问题;而在kubelet的部分版本升级中,也可能会因版本的升级进而导致已经运行的容器服务发生重启;而在特殊的生产环境中类似的操作直接引起的服务重启是需要我们尽可能去规避的;
此篇内容即是对kubelet版本升级与container自动重启这一关联过程原因分析以及供参考方案的简单介绍。
环境基本信息:
操作系统: CentOS 7
kubelet当前版本:1.7.4
目标kubelet版本:1.9.1
现象
当前kubelet版本1.7.4中已持续运行一些容器服务,在对kubelet版本升级1.9.1后,所升级机器上已持续运行多天的container服务出现自动重启,重启之后继续提供服务
原因
因为container的自动重启发生在kubelet版本升级后,所以需要沿kubelet启动后的接口函数调用链进行定位,以确认container重启的根本原因.
总体调用链关系图:(右下角computePodActions() 函数中containerChanged()函数即为决定container服务是否重启的判断逻辑)
高清无码图下载(https://pan.baidu.com/s/1dtMmbO)
链路跟踪过程:(整体kubelet调用关系比较复杂,后面仅针对问题的场景关键链路进行分析)
@kubernetes-1.9.1/cmd/kubelet/kubelet.go (@表示所处的源码文件,后面意义相同)
启动kubelet 的main()入口,风格上保持主入口代码最小化的设计风格
功能上仅是读取config初始化配置结构后调用真正的server中的Run()
@kubernetes-1.9.1/cmd/kubelet/app/server.go
判断启动模式,同时初始化证书相关和心跳等,以及进行健康度的检查
在RunKubelet中启动协程,进入真正的更新处理流程kubelet中的Run主体
@kubernetes-1.9.1/pkg/kubelet/kubelet.go
与场景现象相关的逻辑处理主入口,依次进行非依赖模块初始化initializeModules(“internal modules that do not require the container runtime to be up”),以及使用kubeClient与APIserver 同步node状态信息和本地网络状态及容器网络状态的检查
status、probe、pleg作用如下图示,可参照如上整体的调用链路简图
@kubernetes-1.9.1/pkg/kubelet/kubelet.go
syncLoop(),处理变化的主循环逻辑,监听3种来源的管道:file, apiserver, http;
只要有消息就会进行处理(监听的也是updates消息队列)
在syncLoopIteration迭代过程中,会根据不同的类型(config\pleg\sync…)进行相应的处理,处理是通过调用对应的Handle*()函数实现的
在各自的处理函数中,针对所有的类别,本质都是调用dispatchWork,只是传入的参数分别为不同的类别而已;如果pod已是终止状态了,则需要同时调用statusManager.TerminatePod(pod) 来通知apiserver状态的更新;
在dispatchWork函数中,会调用具体执行单元worker中UpdatePod函数,此时已将变化封装的信息交由执行单元worker。
@kubernetes-1.9.1/pkg/kubelet/pod_workers.go
在UpdatePod中,主要是启动持续处理逻辑managePodLoop(),此函数只运行一次,执行后会启动一个循环来处理syncPodFn指向函数。如果希望退出循环则如下2个函数被调用时会中止这个循环:ForgetWorker()与 ForgetNonExistingPodWorkers();控制这2个函数是否被调用则依赖于housekeepingCh 消息队列的处理。
managePodLoop会调用syncPodFn函数
syncPodFn指向kubelet.go中的syncPod处理函数。
@kubernetes-1.9.1/pkg/kubelet/kubelet.go
对于status, mkdir与volume简述如下
最后调用容器运行时的SyncPod函数,以便对POD中容器进行操作。
@ kubernetes-1.9.1/pkg/kubelet/container/runtime.go
containerRuntime.SyncPod进而调用
@ kubernetes-1.9.1/pkg/kubelet/kuberuntime/kuberuntime_manager.go
调用computePodActions()判断哪些container发生了变化,以便后面对变化的container进行处理(如重启等)
在computePodAction()中会对POD中运行的container进行判断,以此为基础来决定后面是否对这个运行的container进行重启(即决定是否重启container的入口判断逻辑)
检查运行时container与pod.spec中是否Hash一致,如果发生变化,则在changes结构中ContainersToStart 加入这个container,以便统一对ContainersToStart数组中的容器进行重启
@ kubernetes-1.9.1/pkg/kubelet/container/helpers.go
对container进行深层次HASH计算
@ kubernetes-1.9.1/pkg/util/hash/hash.go
使用spew对container对象元素进行字符拼接(github.com/davecgh/go-spew/spew)
由此可见,在版本升级启动后,定时触发的containerChanged()函数对container结构元素进行了内容上的HASH比对校验,从而将变化的container加入到ContainersToStart列表。
自syncPodFn至DeepHashObject链路如下所示:
由于DeepHashObject是对container结构的序列化进行HASH计算,所以对比升级前后版本Container结构不难发现,成员增了VolumeDevices等新成员。
所以在对container进行HASH计算时出现了新旧版本计算HASH值的不一致进而引起重启container服务。
方案
由于重启container是基于container HASH值的比较来判定的,所以可以将kubelet版本信息与启动时间记录并持久化,当container变化后计算HASH的时候,参考持久化的信息辅助决定是否忽略重启的条件。基本参考方案示例流图如下
更新源代码的补丁文件@ https://github.com/cloudusers/UpdateKubeletVersionIgnoreContainerRestart
适用场景及注意事项
适用于在kubelet版本升级情况下,container服务持续运行不希望重启的业务场景;同时因对源码的改动也引入了如下的注意事项或复杂点:
(1)版本升级前对比不同版本container结构的变化情况以便了解是否升级对容器HASH计算产生影响;
(2)目前采用的记录并缓存kubelet版本及启动时间信息至checkpoint持久化本地文件,所以版本升级前文件的损坏可能使得容器服务依旧会重新启动(因checkpoint内容较少,可以临时手动生成);
(3)在每次版本升级前需要对新版本进行源码更新重新编译生成二进制执行文件,过程稍显复杂。
综上所述,简单介绍了自kubelet启动至container服务重启的链路调用关系,同时确认版本升级后引起容器重启的判断条件及处理逻辑,以及提供可参考的避免因版本升级引起重启容器的解决方案;
另一方面简单说明了源码修改后所适用的场景及注意事项。因目前还处在研发阶段,但是我们可以发现,通过规避容器服务因版本升级导致的重启可以解决生产环境中的关键业务无服务中断的问题。