Kubelet组件运行在Node节点上,维持运行中的Pods以及提供kuberntes运行时环境,主要完成以下使命: 1.监视分配给该Node节点的pods 2.挂载pod所需要的volumes 3.下载pod的secret 4.通过docker/rkt来运行pod中的容器 5.周期的执行pod中为容器定义的liveness探针 6.上报pod的状态给系统的其他组件 7.上报Node的状态
整个kubelet可以按照上图所示的模块进行划分,模块之间相互配合完成Kubelet的所有功能.下面对上图中的模块进行简要的介绍. Kubelet对外暴露的端口,通过该端口可以获取到kubelet的状态
10250 kubelet API –kublet暴露出来的端口,通过该端口可以访问获取node资源以及状态,另外可以配合kubelet的启动参数contention-profiling enable-debugging-handlers来提供了用于调试和profiling的api
4194 cAdvisor –kublet通过该端口可以获取到本node节点的环境信息以及node上运行的容器的状态等内容
10255 readonly API –kubelet暴露出来的只读端口,访问该端口不需要认证和鉴权,该http server提供查询资源以及状态的能力.注册的消息处理函数定义src/k8s.io/kubernetes/pkg/kubelet/server/server.go:149
10248 /healthz –kubelet健康检查,通过访问该url可以判断Kubelet是否正常work, 通过kubelet的启动参数–healthz-port –healthz-bind-address来指定监听的地址和端口.默认值定义在pkg/kubelet/apis/kubeletconfig/v1alpha1/defaults.go
PLEG全称为PodLifecycleEvent,PLEG会一直调用container runtime获取本节点的pods,之后比较本模块中之前缓存的pods信息,比较最新的pods中的容器的状态是否发生改变,当状态发生切换的时候,生成一个eventRecord事件,输出到eventChannel中. syncPod模块会接收到eventChannel中的event事件,来触发pod同步处理过程,调用contaiener runtime来重建pod,保证pod工作正常.
跟其他Manager之间的关系,podManager会被statusManager/volumeManager/runtimeManager所调用,并且podManager的接口处理流程里面同样会调用secretManager以及configMapManager.
上面所说的模块可能只是kubelet所有模块中的一部分,更多的需要大家一起看探索.
下面进一步的深入分析kubelet的代码 Kubelet负责pod的创建,pod的来源kubelet当前支持三种类型的podSource - FileSource: 通过kubelet的启动参数–pod-manifest-path来指定pod manifest文件所在的路径或者文件都可以.Kubelet会读取文件里面定义的pod进行创建.常常我们使用来定义kubelet管理的static pod - HTTPSource: 通过kubelet的启动参数–manifest-url –manifest-url-header来定义manifest url. 通过http Get该manifest url获取到pod的定义 - ApiserverSource: 通过定义跟kube-apiserver进行通过的kubeclient, 从kube-apiserver中获取需要本节点创建的pod的信息.
Kubelet如何同时处理这三种podSource里面定义的pod进行处理的.在src/k8s.io/kubernetes/pkg/kubelet/kubelet.go:254的makePodSourceConfig中分别是处理三种podSource的启动参数.
三种source的实现类似,分别启动goroutinue,周期性的查看是否有新的数据来源,如果发现获取到新的数据,生成PodUpdate对象,输出到对应的channel里面.
会针对每种类型创建对应的Channel.
cfg.Channel(kubetypes.FileSource)
cfg.Channel(kubetypes.HTTPSource)
cfg.Channel(kubetypes.ApiserverSource)
Channel存储在PodConfig.mux.sources里面 其中channel中传递的对象定义如下
type PodUpdate struct {
Pods []*v1.Pod
Op PodOperation
Source string
}
Op为kubetypes.SET Source表示pod的来源,可能的值为HTTPSource|FileSource|ApiserverSource, 进一步的分析代码,发现定义chanel的时候,同时也定义gorountine用来watch该channel的变化 go wait.Until(func() { m.listen(source, newChannel) }, 0, wait.NeverStop)
func (m *Mux) listen(source string, listenChannel <-chan interface{}) {
for update := range listenChannel {
m.merger.Merge(source, update)
}
}
原来是将3个channel的对象merge到podConfig.updates中.这个地方merge会对podUpdate进行预处理,处理流程可以仔细看podStorage.merge().会将事件中包含的pods与本地内存中存储pods信息进行分析,将podUpdate分成adds.updates,deletes,removes,reconsiles五类,并分别更新kubetypes.PodUpdate.Op的操作. 对于podSource中生成的podUpdate,如果初次进入该流程,一开始podUpdate.Op=kubetypes.SET, 将会podSource中定义的pod将会addPods里面,podUpdate.Op=kubetype.ADD.
定义的podConfig保存在kubeDeps.PodConfig中.
进一步跟进kubelet源码,自然想到的是谁会从channel中获取PodUpdate进行处理,进行pod同步. Kubelet的主流程里面会启动gorountiue执行如下代码
func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) {
glog.Info("Starting kubelet main sync loop.")
// The resyncTicker wakes up kubelet to checks if there are any pod workers
// that need to be sync'd. A one-second period is sufficient because the
// sync interval is defaulted to 10s.
syncTicker := time.NewTicker(time.Second)
defer syncTicker.Stop()
housekeepingTicker := time.NewTicker(housekeepingPeriod)
defer housekeepingTicker.Stop()
plegCh := kl.pleg.Watch()
for {
if rs := kl.runtimeState.runtimeErrors(); len(rs) != 0 {
glog.Infof("skipping pod synchronization - %v", rs)
time.Sleep(5 * time.Second)
continue
}
kl.syncLoopMonitor.Store(kl.clock.Now())
if !kl.syncLoopIteration(updates, handler, syncTicker.C, housekeepingTicker.C, plegCh) {
break
}
kl.syncLoopMonitor.Store(kl.clock.Now())
}
}
其中updates就是podConfig.updates中定义的channel. 同时kl.pleg.Watch()是pleg模块定义的事件channel, SyncHandler定义
type SyncHandler interface {
HandlePodAdditions(pods []*v1.Pod)
HandlePodUpdates(pods []*v1.Pod)
HandlePodRemoves(pods []*v1.Pod)
HandlePodReconcile(pods []*v1.Pod)
HandlePodSyncs(pods []*v1.Pod)
HandlePodCleanups() error
}
syncLoopIteration方法是一个channel中事件处理中心,处理从跟pod生命周期创建相关的channel中获取事件,之后进行转发到对应的处理函数中.这个对于理解kubelet对于pod的管理至关重要. 触发同步的事件channel主要包括 1.configCh pod的配置改变 2.PLEG模块中状态更新事件 3.1s为周期的同步时钟 syncPod 4.2s为周期的执行全局清理任务的始终 CleanupPod
启动了goroutinue循环调用PodCfg.Updates()中的Channel中获取PodUpdate.进入消息中专分支流程. 1.<-configCh && u.Op=kubetype.ADD a)执行handler.HandlePodAdditions, handler.HandlePodAdditions()的实现在kubelt.HandlePodAdditions(pods []*v1.Pod)
在Kubelt.HandlePodAdditions中再一次分发podUpdate并将kl.probeManager(AddPod(pod)),执行dispatchWork. 之后将updatePodOption初始化,添加到podWorkers.podUpdates的channel中.
dispatchWork中将updatePodOptions定义成如下结构体
type UpdatePodOptions struct {
// pod to update
Pod *v1.Pod
// the mirror pod for the pod to update, if it is a static pod
MirrorPod *v1.Pod
// the type of update (create, update, sync, kill)
UpdateType kubetypes.SyncPodType
// optional callback function when operation completes
// this callback is not guaranteed to be completed since a pod worker may
// drop update requests if it was fulfilling a previous request. this is
// only guaranteed to be invoked in response to a kill pod request which is
// always delivered.
OnCompleteFunc OnCompleteFunc
// if update type is kill, use the specified options to kill the pod.
KillPodOptions *KillPodOptions
}
同时启动goroutinue用于处理managePodLoop,在managePodLoop将会依次处理channel中的podUpdate.最终将会调用kubelet中定义的func (kl *Kubelet) syncPod(o syncPodOptions).在kubelet的syncPod中实现了调用底层其他模块来完成pod状态的同步.
1.kubelet.syncPod记录podWorkerStartLatency监控指标,该指标用来统计pod被node所创建延迟的时间 2.执行kubelet中定义的podSyncHandler.ShouldEvict(),当前podSyncHandler定义有activeDeadlineHandler,该handler对应pod的spec.activeDeadlineSeconds定义进行处理,如果pod中定义该字段,则要求pod在该字段定义的时间内完成创建过程. 3.generateAPIPodStatus根据pod的信息来生成PodStatus结构体 4.kubelet.canRunPod检查是否本节点可以运行该pod,检查通过softAdmitHandler进行定义,另外对于pod是否具有allowPrivileged的权限.其中softAdmitHandler的定义在kubelet的启动流程中,分别为
lifecycle.NewPredicateAdmitHandler
lifecycle.NewAppArmorAdmitHandler
lifecycle.NewNoNewPrivsAdmitHandler
5.statusManager.SetPodStatus更新缓存pod的状态信息,触发状态更新 6.触发kl.containerManager.updateQOSCgroups()更新pod的cgroup设置.其中kl.containerManager的定义为cm.NewContainerManager()中创建的type containerManagerImpl struct实现的interface. 继续分析代码最终调用的函数是qosContainerManagerImpl.UpdateCgroups(),配置顶层的QosCgroup的相关内容. 7.调用kl.containerManager.NewPodContainerManager().EnsureExists(),配置pod的cgroup的信息. 只有在kubelet的启动参数–cgroups-per-qos为true的时候,会执行podContainerManagerImpl.EnsureExists()来创建将pod中设置对于资源的限制去对应的cgroup.详细的创建是由cgroupManager来完成. 8.创建pod所使用的podDir,podVolumesDir以及podPluginDir目录 9.volumeManager.WaitForAttachAndMount(pod) volumeManager将会把volume挂载到pod运行的宿主机上面. 10.调用kl.getPullSecretsForPod(),如果pod中定义了spec.ImagePullSecrets,则获取资源对象中定义的内容. 11.最后调用kl.containerRuntime.SyncPod(),kl.containerRuntime的定义可以参考kubelet的启动流程.如果runtime是docker,那么SyncPod()的定义为kubeGenericRuntimeManager, src/k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_manager.go:568,在该流程中分别是创建sandbox,之后创建create init/containers.
Node节点运行运用的pod以及系统基础组件,这里的资源主要指的是node节点上的cpu,memory,storage. 用户的pod存在资源使用随着时间变大的情况,可能会影响到其他正常工作的pod以及node节点上的其他系统组件等,如何在该场景下提高node节点的稳定性是一个需要探索的问题. 首先,先Kubernetes中相关的概念
Cgroups
Cgroups是control groups的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:cpu,memory,IO等等)的机制。 跟容器相关的功能主要涉及以下 · 限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)。 · 进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。 · 记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间。 · 进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。
Limits/request
Request: 容器使用的最小资源需求,作为容器调度时资源分配的判断依赖。只有当节点上可分配资源量>=容器资源请求数时才允许将容器调度到该节点。但Request参数不限制容器的最大可使用资源。 Limit: 容器能使用资源的资源的最大值,设置为0表示使用资源无上限。 当前可以设置的有memory/cpu, kubernetes版本增加了localStorage在1.8版本中的策略.
Pod Qos
Qos服务质量分成三个级别 BestEffort:POD中的所有容器都没有指定CPU和内存的requests和limits,默认为0,不限制资源的使用量,那么这个POD的QoS就是BestEffort级别 Burstable:POD中只要有一个容器,这个容器requests和limits的设置同其他容器设置的不一致,requests < limits, 那么这个POD的QoS就是Burstable级别 Guaranteed:POD中所有容器都必须统一设置了limits,并且设置参数都一致,如果有一个容器要设置requests,那么所有容器都要设置,并设置参数同limits一致,requests = limits. 那么这个POD的QoS就是Guaranteed级别. Kuberntes管理的node资源中cpu为可压缩资源,当node上运行的pod cpu负载较高的时候,出现资源抢占,会根据设置的limit值来分配时间片. Kubernetes管理的node资源memory/disk是不可压缩资源,当出现资源抢占的时候,会killer方式来释放pod所占用的内存以及disk资源. 当非可压缩资源出现不足的时候,kill掉pods的Qos优先级比较低的pods.通过OOM score来实现,Guaranteed pod,OOM_ADJ设置为-998, BestEffort 设置的OOM_ADJ为1000, Burstable级别的POD设置为2-999.
Evict策略
当系统资源不足的时候,kubelet会通过evict策略来驱逐本节点的pod来使得node节点的负载不是很高, 保证系统组件的稳定性. 当前支持的驱逐信号Eviction Signal为 memory.available nodefs.available nodefs.inodesFree imagefs.available imagefs.inodesFree
kubelet将支持软硬驱逐门槛, 操作管理员通过设置Kubelet的启动参数–eviction-soft –eviction-hard 来指定, 硬驱逐阈值没有宽限期,如果观察到,kubelet将立即采取行动来回收相关的饥饿资源。 如果满足硬驱逐阈值,那么kubelet会立即杀死pods,没有优雅的终止。软驱逐阈值将驱逐阈值与所需的管理员指定的宽限期配对。kubelet不采取任何措施来回收与驱逐信号相关的资源,直到超过宽限期。
以内存导致驱逐的场景来详细说明 让我们假设操作员使用以下命令启动kubelet: –eviction-hard=”memory.available<100Mi” –eviction-soft=”memory.available<300Mi” –eviction-soft-grace-period=”memory.available=30s” kubelet将运行一个同步循环,通过计算(capacity-workingSet)从cAdvisor报告,查看节点上的可用内存。 如果观察到可用内存降至100Mi以下,那么kubelet将立即启动驱逐。 如果观察到可用内存低于300Mi,则会在高速缓存中内部观察到该信号时记录。 如果在下一次同步时,该条件不再满足,则该信号将清除缓存。 如果该信号被视为满足长于指定时间段,则kubelet将启动驱逐以尝试回收已满足其逐出阈值的资源。 Pods的驱逐策略 如果已经达到逐出阈值,那么kubelet将启动逐出pods的过程,直到观察到信号已经低于其定义的阈值。
驱逐顺序如下: 1. 对于每个监测间隔,如果已经达到逐出阈值 2. 找候选pod 3. 失败pod 4. 阻止直到pod在节点上终止 kubelet将实施围绕pod质量服务类定义的默认驱逐策略。
它将针对相对于其调度请求的饥饿计算资源的最大消费者的pod。它按照以下顺序对服务质量等级进行排序。
关于imagefs/nodefs导致的资源的驱逐详细参考https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/kubelet-eviction.md#enforce-node-allocatable
参考 https://speakerdeck.com/luxas/kubernetes-architecture-fundamentals https://kubernetes.io/docs