juicefs-csi中pod mount的annotation与label分析
juicefs-csi实现了三种挂载方式:pod mount, sidecar mount, process mount。最常用的便是pod mount,这种方式支持带业务平滑升级,并且支持多个业务pod挂载点由同一个mount pod管理,从而节约资源开销。
本文将通过对pod mount的annotion和label,以及相关的实现,来进一步深入理解juicefs-csi的设计思想。
参考:
annotations
1. juicefs-<HASH>: 宿主机的挂载点路径
RefrenceKey,用于描述mount pod对业务pod的引用信息。
eg:
juicefs-e0b59010f7a24ee15c8625a12727a21ef13f0951f55da74a2edf56a /var/lib/kubelet/pods/566a11ab-6af1-4597-ab5c-14735974f7ce/volumes/kubernetes.io~csi/juicefs-pv2/mount
此处的HASH是根据挂载点路径通过GetReferenceKey(target)
计算出来的。
这个annotation用于描述当前mount pod所需维护的挂载点。若只有一个业务pod通过 PV的方式引用到这个mount pod,则只会有一个。若有多个业务pod通过引用同一个PV的方式引用到这个mount pod,则会有多个。【另:这种用法其实不建议,因为缺乏分布式锁,业务对相同资源可能会相互覆盖,除非业务自己对文件有锁保护】
可以用于定位某个挂载点路径,从而实现需要该路径的逻辑,如:
- 添加时机:在PodMount的
createOrAddRef(ctx, podName,jfsSetting,appinfo)
中,在需要新建mount pod的时候会将jfsSettin.TargetPath
作为value,赋值给juicefs-<HASH_OF_TARGET_PATH>
的key的annotation中。
可以用于定位某个挂载点路径,从而实现需要该路径的逻辑,如:
-
删除时机:在PodDriver
checkAnnotations(ctx, pod)
中,遍历该pod所有的Annotations得到k,v, 若k==GetReferenceKey(v)
,则说明这个pod包含这个引用信息。之后在podDriver.mit.deletedPods[targetUid]
表中查询,若该target已经被标记为删除状态,则可以删除当前的annotation,表示不再被该target引用。 -
修改时机:在PodDriver的
applyConfigPatch(ctx,pod)
中,获取老的pod中的anno,更新到新的pod中。 -
在PodDriver
podDeletedhandler(ctx, pod)
中,会遍历所有anno,若发现是reference key,则记录到existTarget\[k]:v
的map类型的局部变量中。若该map的长度为0,且这个uniqueId
没有可用的mount pod (根据uniqueId和upgradeUUID
,无法在podDriver.uniqueIdIndex[uinqueId]
中找到status为podDeleted
或podComplete状态
),则调用cleanBeforeDeleted(ctx,pod)
进行清理。-
通过pod获取挂载点
-
umount path
-
清理挂载点
-
清理cache
-
关闭fd,清理 socket。(调用
passfd.GlobalFds.StopFd(ctx, pod)
)。
-
-
在PodDriver
recover(ctx,pod,mntPath)
中,会遍历所有pod的anno,对于reference key,会对挂载点进行恢复:-
解析挂载点target:
p.mit.resolveTarget(ctx, target)
-
恢复挂载点:
p.recoverTarget(ctx, pod.Name, mntPath, mi.baseTarget, mi)
-
根据ti.status的不同状态,进行不同的处理:
-
targetStatusNotExist
,但target在/proc/self/mountinfo
文件中。场景为本地target存在,但target bind的源不存在,会导致用户删除pod失败。此处处理一下可以避免此场景。 -
targetStatusCorrupt
,会先判断几种意外情况(如source path/proc/self/mountinfo
和bind不一致、pod被删除)跳出,之后进行挂载点恢复:-
umount target,否则mountinfo文件会无限增加,需要通过umount清理无效mountinfo。
-
对于subPath的处理
-
调用
p.Mount(sourcePath, ti.target,"none",mountOption)
进行挂载。
-
-
遍历子路径,恢复挂载点:err := p.recoverTarget(ctx, pod.Name, mntPath, ti, mi);
-
-
-
在podMount的
UmountTarget(ctx,target,podName)
时,会先调用CleanupMountPoint()
,然后根据target获取refrenceKey
,若存在,则删除此anno。 -
在podMount的
AddRefOfMount(ctx,target,podName)
中,增加新的refrenceKey
的anno。 -
在webhook的
EvictPodHandler(ctx,request)
中,会判断如果有refrenceKey
,则不去驱逐pod。
2. juicefs-uniqueid: <PV_volumeHandler>
若为static provision,则是volumeHandler
(即pv name);若为dynamic provision,则为Storage Class Name。这在集群中肯定是唯一的。
eg:
juicefs-uniqueid: juicefs-pv2
这个anno可以做以下事情:
-
所有需要获取pvName,以及通过pvName做一些操作的(如拼接挂载点路径),都可以由此获得。
-
当集群中有许多mount pod,需要找到某个mount pod对应的挂载点路径时,通过这个uniqueid进行过滤匹配。
这个anno的值实际上和label: volume-id
的值是相同的。
相关设计:
-
设置时机:在podMount
JMount()
时,新建mount pod后会打上volume-id的label。 -
PodDriver结构体维护了一个
uniqueIdIndex[string][]podWithUpgradeUUID
:
golang
type PodDriver struct {
lock sync.Mutex
Client *k8sclient.K8sClient
handlers map[podStatus]podHandler
uniqueIdIndex map[string][]podWithUpgradeUUID
mit mountInfoTable
mount.SafeFormatAndMount
}
type podWithUpgradeUUID struct {
upgradeUUID string
status podStatus
}
这个结构体只在controller/pod_driver.go中使用。controller pod中进程如下:
shell
juicefs-csi-driver --endpoint=unix:///var/lib/csi/sockets/pluginproxy/csi.sock --logtostderr --nodeid=vm-146-97-tencentos --leader-election --config=/etc/config/config.yaml
在controller的Reconcile()过程中进行增删改查。
调用controller Reconcile()接口的时机是mount pod的创建、状态变化、配置更新、删除、节点故障恢复等场景。PodController是个全局实例。用于管理pod的生命周期。
-
uniqueIdIndex 添加时机:在Controller启动时,NewPodDriver(k8sClient, mounter, poolList)中,会遍历所有的podList,若有volume-id的label,则会添加到driver.uniqueIdIndex[uniqueId]中,其value为podWithUpgradeUUID{ upgradeUUID, status}。
-
uniqueIdIndex 添加时机2: 在PodDriver
recordPod(uniqueId, upgradeUUID)
中,会将传入的uniqueId, upgradeUUID
添加到p.uniqueIdIndex\[uniqueId]
中。而调用recordPod()
的时机为podCompletehandler()
和podDeletedHandler()
。 -
uniqueIdIndex 查询时机:可以通过
getUniqueMountPod(ctx, uniqueId)
来检查指定uniqueId
的pod是否存在。若该表中能查到,且状态不是podDeleted
和podComplete
,则说明该mount pod存在。否则不存在。这主要用于在PodDrivercleanBeforeDeleted(ctx, pod)
中做清理使用。当指定uniqueId的pod不存在,才能清理cache。
3. juicefs-uuid: <HASH>
eg:
juicefs-uuid: 67d847ef-acb2-420e-8664-d6d0e4f434a2
-
设置时机:在
pod\_mount.go
中,JMount()
的时候通过setUUIDAnnotation(ctx, podName, uuid)
来设置。uuid通过GetJfsVolUUID(ctx, jfsSetting)
方法生成,本质上是获取juicefs status中的UUID
字段。这是由juicefs生成的唯一字段,可以用于区分文件系统。- 之后会将其保存到配置文件jfsSetting中。
-
使用时机:在PodDriver
CleanUpCache(ctx,pod)
中,会通过anno获取uuid和uniqueId,然后调用podMnt.CleanCache(ctx,image,uuid,uniqueId,cacheDirs)
清理缓存。-
对于PodMount,会获取配置文件jfsSetting,然后创建一个Job(具体定义在
genCleanCachePod()
中),用于清理rm -rf /var/jfsCache/\*/chunks
,对应于hostPath的具体目录。 -
对于ProcessMount。待续。
-
-
缓存的管理:cachedir-0(详见cache.md)
-
允许在mountPodPatch-mountOptions中,通过cache-dir和cache-size配置hostPath形式的缓存目录。默认目录为
/var/jfsCache
,是系统目录,因此建议配置为指定的非系统盘目录。-
使用hostPath的好处在于管理和观测都方便,不过mount pod可能随着业务容器被分配到其他节点,这样的话缓存就会失效,并且原有的缓存也需要清理。配置方法:
-
但如果集群中的所有节点都用于运行mount pod,那么由于每个宿主机都持有大致相同的缓存,那么pod漂移问题可能也影响不大。
-
-
也可以使用PVC作为缓存目录。这种形式可以有很好的隔离性,并且即使业务pod漂移,由于PVC绑定关系不变,缓存也是有效的。
-
不过这里的PVC得是云服务提供,从而在任意节点都能访问到,而不能是hostPath形式(自然)。
-
感觉这种场景应该不太多,缓存本身是为了加速,这里引入外部云存储的依赖,会变成拖累。
-
-
-
缓存的清理:因为缓存是昂贵的,因此设计上mount pod退出时默认不会清理缓存。可以通过配置PV的
volumeAttributes: juicefs/clean-cache:"true"
的方式实现退出时默认清理缓存。 -
由PodMount
CleanCache()
定义清理缓存步骤。如上述所示。
labels
1. app.kubernetes.io/name: juicefs-mount
用于过滤mount pod。
当label selector需要查找juicefs mount pod的时候,不用全量遍历pod,而是通过这个label来过滤。
2. juicefs-hash: <HASH>
juicefs-hash
用于描述配置的改动。
通过GenHashOfSetting(JfsSetting)
来生成,简单来讲就是将JfsSetting
的key按照固定规则排序,然后将其内容进行哈希,得到一个哈希值。
eg:
juicefs-hash: 1733a399cf220ea29a9bc6edd6e95e40b0aeb94fa999a21bcb3dc3856e8273b
若juicefs-hash为空,则表示没有配置,是个异常场景(在NewPodUpgrade()
中有检查)。
若setting.HashVal
与pod label中的juicefs-hash不同,则说明有配置变更,需要做一些对应操作。如更新secret、在genMountPodName
时用于清理过时的引用。
3. juicefs-upgrade-uuid: <HASH>
在pod_mount.go的JMount()
的时候生成。这个生成没有什么讲究,直接用uuid.NewUUID()
来生成,然后保存在jfsSettin.UpgradeUUID
中。
对于平滑升级前后的mount pod来说,这个label是相同的。那么这是如何实现的呢?
是通过GenSettingAttrWithMountPod(ctx,client,mountPod)
,从旧mount pod获取所有配置,保存在JfsSetting中。在处理的时候会优先使用旧mount pod的label对应的值。若为空,则会使用juicefs-hash对应的值。(感觉像是为了兼容旧版本?)
在newMountPod(ctx, pod, newPodName)
-> applyConfigPatch(ctx, pod)
的时候调用。
eg:
juicefs-upgrade-uuid: c3bc9d3c-fa0a-494e-9f17-58683c00ea70
为什么需要这个标签呢?
-
会通过
GetUpgradeUUID(pod)
获取某个pod的upgradeUUID
。 -
会通过
GetCommPath(bashPath, pod)
结合upgradeUUID
来拼接获取fuse_fd_comm.1
的路径。这样新旧mount pod都可以找到相同的路径。 -
在全局的表结构Fds中,维护一个map:
fds[upgradeUUID]: *fd
。用于记录通过upgradeUUID
区分开来的所有mount pod的fd表。
go
type Fds struct {
client *k8s.K8sClient
globalMu sync.Mutex
basePath string
fds map[string]*fd
}
type fd struct {
done chan struct{}
fuseFd int
fuseSetting []byte
sid uint64
serverAddress string // server for pod
serverAddressInPod string // server path in pod
}
4. volume-id: juicefs-pv2
对应于PV定义中的volumeHandler
.
相关: uniqueId通过getUniqueId(ctx, volumeId)
来计算。当是dynamic provision时,UniqueId为SC name。当为static provision时,UniqueId为volumeId。
pkg/controller/pod_driver.go
中会维护driver.uniqueIdIndex[unqiueId]: podWithUpgradeUUID{ upgradeUUID, status}
的映射关系,用于便捷地查询某个mount pod的状态,如getAvailableMountPod(uniqueId, upgradeUUID)
。