kubernetes configmap热更新源码简析

1 概述:

1.1 环境

版本信息如下:
a、操作系统:centos 7.6
c、kubernetes版本:v1.15.0


1.2 configmap热更新原理概述

configmap(secret的热更新也是一个原理)是kubernetes支持的卷的一种,底层原理是kubelet根据configmap中的内容在宿主机上写成若干文件(目录默认是/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>),最终通过绑定挂载的方式映射到容器内部,在宿主机上可直接修改这些文件,容器也能实时看见,因为是同一个文件。kubelet的一个组件叫volume manager,此volume manager的reconciler协程专门执行块设备的attach/detach、目录的mount/remount/unmount。reconciler协程的mount操作,就包含了更新/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>/这种目录下的文件的内容。热更新每隔多久执行一次,也是可以通过启动参数设定。


2 影响configmap热更新的参数:

1)–sync-frequency参数,这是kubelet同步pod状态的时间间隔,也是重新挂载卷(例如configmap热更新)的时间间隔。

fs.DurationVar(&c.SyncFrequency.Duration, "sync-frequency", c.SyncFrequency.Duration, "Max period between synchronizing running containers and config")

3 源码简析:

3.1 volume manager(kubelet的一个属性)的初始化

func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,)

{

	/*
	其他代码
	*/

	// 为kubelet的对象设置volumeManager
	klet.volumeManager = volumemanager.NewVolumeManager(
		kubeCfg.EnableControllerAttachDetach,
		nodeName,
		klet.podManager,
		klet.statusManager,
		klet.kubeClient,
		klet.volumePluginMgr,
		klet.containerRuntime,
		kubeDeps.Mounter,
		klet.getPodsDir(),
		kubeDeps.Recorder,
		experimentalCheckNodeCapabilitiesBeforeMount,
		keepTerminatedPodVolumes)
		
	return klet, nil	

}


3.2 启动volume manager

kubelet在启动过程中会启动众多组件,其中一个重要组件就是volume manager。

func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
	/*
		其他代码
	*/

	// 启动volume manager
	go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop)
	
	/*
		其他代码
	*/
	
	// 启动kubelet的主循环
	kl.syncLoop(updates, kl)
}

volume manager的启动,又会启动3个协程。本文文章主要关心第二个协程:reconciler启动的协程。


func (vm *volumeManager) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) {
	defer runtime.HandleCrash()

	// 1)启动volume manager的desiredStateOfWorldPopulator
	go vm.desiredStateOfWorldPopulator.Run(sourcesReady, stopCh)

	// 2)启动volume manager的reconciler
	go vm.reconciler.Run(stopCh)

	if vm.kubeClient != nil {
		// 3)启动volume manager的volumePluginMgr
		vm.volumePluginMgr.Run(stopCh)
	}

	<-stopCh
	klog.Infof("Shutting down Kubelet Volume Manager")
}

3.3 启动volume manager的reconciler

reconciler协程(100毫秒的循环,因为rc.loopSleepDuration=100毫秒)主要负责卷的attach/detach和mount(remount)/unmount。
configmap的热更新也是这个协程做的。


func (rc *reconciler) Run(stopCh <-chan struct{}) {
	wait.Until(rc.reconciliationLoopFunc(), rc.loopSleepDuration, stopCh)
}

func (rc *reconciler) reconciliationLoopFunc() func() {
	return func() {
		rc.reconcile()
		
		if rc.populatorHasAddedPods() && !rc.StatesHasBeenSynced() {
			klog.Infof("Reconciler: start to sync state")
			rc.sync()
		}
	}
}

3.4 reconciler的reconcile() 方法

核心业务逻辑是 reconcile()方法,它会不断地遍历desiredStateOfWorld和actualStateOfWorld,然后执行一系列的attach/detach和mount(remount)/unmount等操作。

func (rc *reconciler) reconcile() {
	
	/*
	    第一步:
		先把不需要挂载的卷执行卸载操作,代码省略
	*/

	/*
	    第二步:
		执行attach操作和mount(remount)操作。
	*/
	for _, volumeToMount := range rc.desiredStateOfWorld.GetVolumesToMount() {
	
		volMounted, devicePath, err := rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)
		volumeToMount.DevicePath = devicePath

		/* 
		这个err对象很重要,不同的err进入不同的if分支,而configmap热更新就是进入其中一个分支。
		很多时候,这个err对象是nil,因此 reconcile()方法其实是空跑。
		*/
				
		if cache.IsVolumeNotAttachedError(err) {
			if rc.controllerAttachDetachEnabled || !volumeToMount.PluginIsAttachable {		
				/*
					kubelet等待其他程序(例如一些控制器)将卷attach到当前节点。
				*/

			} else {
				/*
					卷没有attach到当前节点,因此kubelet将卷attach到当前节点。
					err := rc.operationExecutor.AttachVolume(volumeToAttach, rc.actualStateOfWorld)
				*/
			}
		} else if !volMounted || cache.IsRemountRequiredError(err) {
		
			/*
				进入此语句,说明卷未挂载,或者已经挂载的卷需要重新挂载
				confingmap、secret等热更新的情景就是在此处实现,isRemount = true。
			*/
			isRemount := cache.IsRemountRequiredError(err)				
			// 执行挂载或重新挂载的操作
			err := rc.operationExecutor.MountVolume(
				rc.waitForAttachTimeout,
				volumeToMount.VolumeToMount,
				rc.actualStateOfWorld,
				isRemount)
				
			/*
				打印一些日志
			*/
				
		} else if cache.IsFSResizeRequiredError(err) &&
			utilfeature.DefaultFeatureGate.Enabled(features.ExpandInUsePersistentVolumes) {
			
			/*
				kubelet对正在使用的卷的文件系统进行扩容
			*/
		}
	}

	// Ensure devices that should be detached/unmounted are detached/unmounted.
	
	/*
	   第三步:
		umount和detach块设备,代码省略
	*/
	
}
func (oe *operationExecutor) MountVolume(
	waitForAttachTimeout time.Duration,
	volumeToMount VolumeToMount,
	actualStateOfWorld ActualStateOfWorldMounterUpdater,
	isRemount bool) error {
	
	fsVolume, err := util.CheckVolumeModeFilesystem(volumeToMount.VolumeSpec)
	if err != nil {
		return err
	}

	// 根据volume的类型来构建generatedOperations对象
	// 挂载卷的方法就保存在generatedOperations对象。
	var generatedOperations volumetypes.GeneratedOperations
	if fsVolume {
		/*
		    这是文件系统卷的情景,configmap等卷会进入这里
		*/
		generatedOperations = oe.operationGenerator.GenerateMountVolumeFunc(
			waitForAttachTimeout, volumeToMount, actualStateOfWorld, isRemount)
	} else {
		/*
		    这是块设备的情景,代码省略
		*/
	}
	if err != nil {
		return err
	}
	
	/*
		其他代码
	*/

	// 底层会启动一个协程来执行generatedOperations对象的Run()方法
	return oe.pendingOperations.Run(
		volumeToMount.VolumeName, podName, generatedOperations)
}

3.5 generatedOperations对象的mountVolumeFunc

挂载卷的方法(mountVolumeFunc)就保存在generatedOperations对象。mountVolumeFunc主要是从卷插件管理volumePluginMgr(它维护着plugin列表)中拿一个插件出来,再从插件中得到一个mouter,mounter执行真正的挂载操作。

type GeneratedOperations struct {
	OperationName     string
	OperationFunc     func() (eventErr error, detailedErr error)
	EventRecorderFunc func(*error)
	CompleteFunc      func(*error)
}
func (og *operationGenerator) GenerateMountVolumeFunc(
	waitForAttachTimeout time.Duration,
	volumeToMount VolumeToMount,
	actualStateOfWorld ActualStateOfWorldMounterUpdater,
	isRemount bool) volumetypes.GeneratedOperations {
		
	/*
		其他代码
	*/

	// 这就是挂载卷的方法,最终作为generatedOperations对象的一个属性
	mountVolumeFunc := func() (error, error) {

		/*
			其他代码
		*/
			
		// 从volume plgin管理器中获取一个volume plgin
		volumePlugin, err := og.volumePluginMgr.FindPluginBySpec(volumeToMount.VolumeSpec)
		if err != nil || volumePlugin == nil {
			return volumeToMount.GenerateError("MountVolume.FindPluginBySpec failed", err)
		}

		/*
			其他代码
		*/

		// volume plgin对象中得到一个mounter
		// 对于configmap这种卷,插件的实现是结构体configMapVolumeMounter
		volumeMounter, newMounterErr := volumePlugin.NewMounter(
			volumeToMount.VolumeSpec,
			volumeToMount.Pod,
			volume.VolumeOptions{})
		
		/*
			其他代码
		*/
		
		// mounter的SetUp(...)方法就是执行挂载操作
		mountErr := volumeMounter.SetUp(volume.MounterArgs{
			FsGroup:     fsGroup,
			DesiredSize: volumeToMount.DesiredSizeLimit,
			PodUID:      string(volumeToMount.Pod.UID),
		})
		
		
		// 更新actualStateOfWorld,标记当前卷已经被挂载
		actualStateOfWorld.MarkVolumeAsMounted(...)
		
		return nil, nil
	}

	/*
		其他代码
	*/
	return volumetypes.GeneratedOperations{
		OperationName:     "volume_mount",
		OperationFunc:     mountVolumeFunc,
		EventRecorderFunc: eventRecorderFunc,
		CompleteFunc:      util.OperationCompleteHook(util.GetFullQualifiedPluginNameForVolume(volumePluginName, volumeToMount.VolumeSpec), "volume_mount"),
	}
}

volumePluginMgr支持的常见的plugin如下:
在这里插入图片描述

在这里插入图片描述

3.6 configMapPlugin和configMapVolumeMounter

从configMapPlugin对象得到一个mounter对象,其实是把插件的getConfigMap方法赋值给mounter对象的getConfigMap属性。

func (plugin *configMapPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, opts volume.VolumeOptions) (volume.Mounter, error) {
	return &configMapVolumeMounter{
		configMapVolume: &configMapVolume{
			spec.Name(),
			pod.UID,
			plugin,
			plugin.host.GetMounter(plugin.GetPluginName()),
			volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))),
		},
		source:       *spec.Volume.ConfigMap,
		pod:          *pod,
		opts:         &opts,
		getConfigMap: plugin.getConfigMap,
	}, nil
}

configMapVolumeMounter的SetUp()方法是真正执行挂载操作(将configmap的内容写到文件)的方法。


func (b *configMapVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error {
	return b.SetUpAt(b.GetPath(), mounterArgs)
}

// 入参dir就是/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>
func (b *configMapVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
	klog.V(3).Infof("Setting up volume %v for pod %v at %v", b.volName, b.pod.UID, dir)

	/*
		其他代码
	*/
	
	// configmap对象可以来自kube-apiserver,也可以来自kubelet中的缓存,取决于configMapManager的实现
	configMap, err := b.getConfigMap(b.pod.Namespace, b.source.Name)
	
	/*
		其他代码
	*/

	payload, err := MakePayload(b.source.Items, configMap, b.source.DefaultMode, optional)
	if err != nil {
		return err
	}
	/*
		其他代码
	*/
	setupSuccess := false

	writerContext := fmt.Sprintf("pod %v/%v volume %v", b.pod.Namespace, b.pod.Name, b.volName)
	writer, err := volumeutil.NewAtomicWriter(dir, writerContext)
	
	// 把configmap的键值对写到入参dir下
	err = writer.Write(payload)
	/*
		其他代码
	*/
	setupSuccess = true
	return nil
}

4 kubelet主循环和volume manager的协作

volume manager的reconciler协程大多数时候是空跑的,只有pod被认为需要重新挂载卷的时候,才会执行重新挂载的操作。那什么时候pod会被认为需要重新挂载已挂载的卷?这个时间间隔和kubelet的主循环有关,和主循环的时间间隔由–sync-frequency参数来设定。每次同步pod的状态,其实都给了该pod重新挂载已挂载卷的机会。当pod被认为需要重新挂载卷,volumeManager的reconciler的reconcile()方法中调用rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)会返回一个err对象,代码会进入重新挂载卷的if语句中。


func (kl *Kubelet) syncPod(o syncPodOptions) error {

	/*
		其他代码
	*/

	
	if !kl.podIsTerminated(pod) {
		// WaitForAttachAndMount(...)等待卷的挂载,同时也给了该pod对已挂载的卷进行重新挂载的机会	
		if err := kl.volumeManager.WaitForAttachAndMount(pod); err != nil {
			kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedMountVolume, "Unable to mount volumes for pod %q: %v", format.Pod(pod), err)
			klog.Errorf("Unable to mount volumes for pod %q: %v; skipping pod", format.Pod(pod), err)
			return err
		}
	}

	/*
		其他代码
	*/
	
}
func (vm *volumeManager) WaitForAttachAndMount(pod *v1.Pod) error {
	/*
		其他代码
	*/

	
	// 标记这个pod的卷要被重新挂载一次(其实是从一个map中删除数据,这样volume manager会以为pod未被处理)
	// volumeManager的reconciler的reconcile()方法中调用rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)会返回一个err对象,从而触发重新挂载的操作
	vm.desiredStateOfWorldPopulator.ReprocessPod(uniquePodName)

	/*
		其他代码
	*/

	klog.V(3).Infof("All volumes are attached and mounted for pod %q", format.Pod(pod))
	return nil
}

5 总结

configmap热更新原理很简单,kubelet有专门的协程来根据configmap对象中的内容来修改/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>/这种目录下的文件。至于协程多久执行一次需要热更新(在volume manager看就是mount volume),这个时间间隔由参数–sync-frequency参数来决定,此参数直接决定同步pod的时间间隔,也间接决定了pod热更新的时间间隔,因为kubelet的主循环同步pod的过程中会标记该pod需要被volume manager重新处理一次。

猜你喜欢

转载自blog.csdn.net/nangonghen/article/details/111147525