ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CephFS在Kubernetes中的挂载实践:从集群配置到应用Pod全链条梳理

CephFS在Kubernetes中的挂载实践:从集群配置到应用Pod全链条梳理 做容器化之后最绕不开的一道坎就是存储。前阵子为了一套跑在K8s上的AI训练平台配置共享存储我在网上翻了不少文档折腾了好几轮最后才把CephFS在一个Kubernetes集群里从Ceph侧一路配到应用Pod的全部细节彻底理清。整个过程踩了不少坑也把很多文档里语焉不详的环节弄明白了。今天这篇就把cephfs在k8s挂载这个主题写完整——从Ceph集群的准备、子卷和授权逻辑到K8s侧Secret、StorageClass、PV/PVC的配置再到节点上报错排查的完整链路按我实际操作过的路径一步步来保证照着能做出来也能知道每一步为什么这么做。1. K8s场景里为什么偏偏选CephFS1.1 容器存储的几个层级CephFS定位在哪K8s存储大概可以分四个层级理解了这个层级你才知道CephFS是来干什么的临时存储emptyDir、hostPath这类Pod一删数据就没了或者只能锁死在某个节点上。适合缓存、日志中转不适合当正式存储。单节点块存储云厂商的云盘、本地盘通过CSI接入变成RWOReadWriteOnce的PV。一个PV同时只能被一个节点读写数据跨节点迁移很别扭。共享文件存储NFS、CephFS、GlusterFS这类POSIX文件系统天然支持RWXReadWriteMany多个节点上的多个Pod可以同时挂载读写同一份数据。对象存储S3兼容接口适合冷数据、备份、静态资源但应用直接读写对象存储和读写文件系统是两码事。CephFS在K8s生态里属于第三层——共享文件系统。它的存在是为了解决一个很具体的问题多个Pod、多个节点要并发读写同一份文件数据并且要像本地文件系统一样使用。1.2 CephFS和Ceph RBD到底有什么区别这是我在交流群里被问过最多的问题。简单说RBD暴露的是块设备。在K8s里挂载时CSI会把块设备格式化成文件系统ext4/xfs再挂载给Pod用。格式化之后这个文件系统依然只是一个节点上的文件系统所以RBD天然是RWO。CephFS暴露的是分布式文件系统本身。客户端通过内核模块kcephfs或者FUSEceph-fuse直接挂载多个节点可以同时挂到同一个文件系统上看到的内容完全一致是真正的共享文件系统。所以如果你需要多个Pod同时读写一个PVCRBD做不到CephFS可以如果只是单Pod独占一块盘RBD更简单也更稳。两者不是替代关系是互补关系。1.3 生产里哪些场景非CephFS不可根据我实际看到的情况这样的场景大致有这么几类AI训练和数据分析平台数据集、checkpoint、训练脚本要同时被多个训练任务读取训练完的产出还要给推理服务读。GitLab Runner / CI流水线多个并发任务要共享一个工作目录缓存。低代码平台、CMS、Web集群用户上传的图片、附件需要所有Web节点都能访问。日志采集与归档Filebeat这类组件从多个节点写入同一个索引切分目录。这些场景的共同点就是RWX。如果你在K8s里遇到PVC的AccessModes必须是ReadWriteMany这种要求又不想自己搭NFS单点CephFS基本是标准答案。提示Dont overengineer。如果你的应用根本不需要多节点共享写用RBD或者云盘就够了。CephFS是共享存储不是更高级的块存储选型前先确认AccessModes。2. 存储端准备先把Ceph集群里的老本盘清楚2.1 版本确认和文件系统状态这步不能省在动K8s之前你得先确认Ceph集群本身是可用的。老规矩先看版本和集群健康状态ceph --version ceph -s ceph fs ls ceph fs statusceph-csi对Ceph版本有明确的兼容要求。就我这些年看下来的经验Ceph Nautilus14.x以上是安全起步线Octopus15.x、Pacific16.x、Quincy17.x都是社区主力验证过的如果你还在用Mimic或者更早建议先升级再谈对接K8s省得后面遇到一堆莫名其妙的兼容问题。ceph fs ls这步最容易被忽视。很多人以为Ceph装了就有CephFS实际上CephFS需要单独创建文件系统它依赖两个Pool数据池和元数据池。如果执行完ceph fs ls之后什么都没输出说明你压根还没建文件系统。创建文件系统的标准命令如下ceph osd pool create cephfs_data 128 ceph osd pool create cephfs_metadata 32 ceph fs new myfs cephfs_metadata cephfs_data两个Pool的PG数量按你集群规模定128和32只是起步值。元数据池的PG可以少一点因为元数据量远小于数据量但性能要求更高SSD优先。2.2 子卷组和子卷多业务共用一套CephFS的正确姿势在K8s接入之前我强烈建议你先理解CephFS的三个层级文件系统filesystem- 子卷组subvolumegroup- 子卷subvolume。子卷组可以理解成CephFS里的目录级配额边界通常一个业务部门一个组。子卷是真正给PVC用的目录可以独立设置配额、快照、权限也可以独立授权给某个客户端用户。创建命令按顺序来# 1. 创建子卷组 ceph fs subvolumegroup create myfs group_k8s # 2. 在子卷组里创建子卷 ceph fs subvolume create myfs vol_ai --group_name group_k8s # 3. 查看子卷 ceph fs subvolume ls myfs --group_name group_k8s注意--group_name参数的位置和写法不少人漏了它导致子卷被建到了默认组里。看子卷详细信息时ceph fs subvolume info会返回一个path字段这个path其实就是CephFS里的相对路径后面K8s静态PV要用它。2.3 授权逻辑K8s节点凭什么能访问CephFSCephFS的访问走的是CephX认证。你需要创建一个专门给CephCSI用的客户端账号并给它最小权限。我自己常用的是ceph auth get-or-create client.k8s mon allow r \ mds allow rws \ osd allow rw tag cephfs data* \ mgr allow rw拆开解释一下mon allow r允许读取Monitor信息csi-provisioner需要知道集群monitor地址。mds allow rws允许CephFS元数据读写这是挂载CephFS的通行证没有rws连目录列表都看不了。*osd allow rw tag cephfs data**限定只能以cephfs数据身份读写OSD数据加了tag cephfs data*后这个账号没法访问RBD块设备安全边界更明确。mgr allow rw让CSI插件能读取集群元数据信息例如CephFS的布局信息。拿到的keyring文件内容一般长这样[client.k8s] key AQDmXG1hAAAABCAAP7e8XvZxxx保存好这个key后面K8s侧的Secret里面要用。如果你习惯直接把admin用户的key塞给CSI也能跑通但生产环境不建议这么干——万一Pod被攻破等于把整个Ceph集群的管理钥匙交出去了。提示如果一个子卷只给某个特定业务用可以用ceph fs subvolume authorize做细粒度授权把权限精确到子卷级别而不是给全部数据池权限。这个我后文讲子卷隔离时再展开。3. K8s侧接入Secret、StorageClass、静态PV三个步骤把挂载打通3.1 把认证信息装进Secret里密钥管理是第一步。你要把刚才生成的client.k8s账号信息放进Kubernetes的Secret里CephCSI会在provision创建卷和node-stage节点挂载两个阶段读取它。创建一个cephfs-secret.yamlapiVersion: v1 kind: Secret metadata: name: cephfs-secret namespace: default type: kubernetes.io/rook stringData: adminID: k8s adminKey: AQDmXG1hAAAABCAAP7e8XvZxxx字段下面详细说adminIDCephX客户端用户名不带client.前缀。adminKey该客户端对应的key。如果你的环境里CSI需要连接多个Ceph集群Secret里还可以加monitors字段用逗号分隔monitor地址列表格式是192.168.1.10:6789,192.168.1.11:6789。创建后验证一下kubectl apply -f cephfs-secret.yaml kubectl get secret cephfs-secret -n default -o yaml3.2 StorageClass动态供给的入口有了Secret我们再建StorageClass。这个是让PVC自动创建子卷的关键apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: csi-cephfs-sc provisioner: cephfs.csi.ceph.com parameters: clusterID: 你的Ceph集群fsid fsName: myfs pool: cephfs_data csi.storage.k8s.io/provisioner-secret-name: cephfs-secret csi.storage.k8s.io/provisioner-secret-namespace: default csi.storage.k8s.io/node-stage-secret-name: cephfs-secret csi.storage.k8s.io/node-stage-secret-namespace: default csi.storage.k8s.io/controller-expand-secret-name: cephfs-secret csi.storage.k8s.io/controller-expand-secret-namespace: default reclaimPolicy: Delete allowVolumeExpansion: trueclusterID怎么拿执行ceph fsid拿到的是一个UUID字符串。这里特别容易错很多人会填成集群名字比如ceph结果provision阶段一直报volumes not found或者cluster id mismatch。pool填的是数据池名字对应上面建文件系统时的cephfs_data。fsName填文件系统名本例是myfs。提示如果你的Ceph集群没有使用默认的FSCID布局或者做了多文件系统管理参数里还可以加fscName之类的高级字段。但刚上手时先别贪多用最简配置跑通一次再说。3.3 静态PV把已有子卷直接交给K8s动态供给适合正式环境批量创建。但有时你不用StorageClass而是想把Ceph侧已经存在的子卷直接挂给K8s用这时需要静态PV。写法如下apiVersion: v1 kind: PersistentVolume metadata: name: csi-cephfs-pv spec: accessModes: - ReadWriteMany capacity: storage: 10Gi csi: driver: cephfs.csi.ceph.com volumeHandle: cephfs-vol-ai-001 nodeStageSecretRef: name: cephfs-secret namespace: default volumeAttributes: fsName: myfs pool: cephfs_data关键点在于volumeHandle必须全局唯一而且要和CephFS子卷路径对应。通常我习惯用subvolume path的hash或者业务ID拼接避免重名冲突。接着建PVC绑定这个PVapiVersion: v1 kind: PersistentVolumeClaim metadata: name: csi-cephfs-pvc spec: accessModes: - ReadWriteMany storageClassName: # 置空走静态绑定 volumeName: csi-cephfs-pv resources: requests: storage: 10GiPVC建完后过几秒就能看到Bound状态。如果一直Pending大概率是PV的volumeHandle和实际CephFS子卷对不上或者Secret里的密钥不对。3.4 挂载器选型内核态还是FUSEStorageClass里有个参数叫mounter默认是kernel可选fuse。这里值得多花点篇幅因为这是生产环境绕不开的选择题。kernel mounter走内核的ceph模块性能好、延迟低、内存占用少。缺点是需要节点内核有ceph模块modprobe ceph能加载才行而且内核版本太老会缺少新特性比如多文件系统支持就可能有限。fuse mounter走用户态的ceph-fuse兼容性好升级更新方便对内核版本不挑剔。代价是性能会有一定损耗内存占用略高。我自己在实在的K8s集群里测试过kernel mounter在顺序读写上的吞吐比fuse高小文件随机读写的差距更明显。所以生产环境我首选kernel但前提是所有节点都要装ceph-common和对应版本的内核模块。如果节点OS比较旧又不想升级退而求其次用fuse更省心。在StorageClass里通过参数指定parameters: mounter: kernel # 或者 fuse4. 挂载排错实录节点上的报错到底在说什么4.1 wrong fs type, bad option 这类错误的完整排查链路这应该是CephFS挂到K8s时报错中出现频率最高的一个。完整报错类似mount error: no such file or directory mount: /var/lib/kubelet/pods/... wrong fs type, bad option, bad superblock on cephfs看到这个先别急着怀疑Ceph集群绝大多数情况是节点侧环境没准备好。我给你一个排查顺序照着走一遍基本能定位确认节点内核有没有ceph模块执行modprobe ceph如果报modprobe: FATAL: Module ceph not found说明内核没编译ceph模块。这种情况在定制内核、裁剪过的发行版上很常见。处理办法是找对应内核版本的ceph模块包装上或者改用fuse mounter。确认节点有没有安装ceph-common执行ceph --version没有就装yum install -y ceph-common # CentOS/Rocky apt install -y ceph-common # Ubuntu/Debian确认节点能不能解析并连通monitorCSI会把monitor地址下发到节点节点挂载时要先连上monitor。如果网络不通报错可能变成mount error: connection refused或者直接timed out。在节点上手动telnet一下monitor端口nc -vz 192.168.1.10 6789。确认keyring文件缓存有效CephCSI在节点上会维护一套临时keyring路径一般在/var/lib/kubelet/plugins/kubernetes.io/csi/...下面。如果密钥从Secret里读出来是错的挂载会报mount error: cephx authentication failed。这种时候去查csi-node-plugin的日志能看到具体原因kubectl logs -n ceph-csi csi-cephfs-node-xxxxx -c csi-node-driver --tail100这里还有个细节节点上多个容器运行时containerd/docker共享内核挂载点所以同一节点只要有一个Pod挂载成功了说明节点侧环境没问题反过来所有Pod都报同样的错误优先怀疑节点侧而不是PVC配置。4.2 API Server健康检查和CSI组件状态别急着查挂载有些时候PVC一直Pending和挂载本身没关系是CSI组件没起来。热搜里提到过master初始化显示the api server is not healthy这类问题——虽然那是集群初始化阶段的事但背后的排查逻辑是通用的先确认K8s控制面和CSI基础设施本身是健康的再往下查存储挂载。在排CephFS问题前我建议先花五分钟做这几件事kubectl get pod -A | grep csi-cephfs # 看看CSI插件本身是否Running kubectl get sc kubectl get pvc -A # 看看PVC的状态Pending还是Bound如果csi-cephfs-provisioner的Pod一直CrashLoopBackOff常见原因是Secret里的密钥错误provisioner连不上Ceph的mgr。clusterID填了字符串名字而不是UUID。Ceph集群版本太旧CSI不兼容。先把这些基础项排查完再进到挂载细节里效率会高很多。我在实际排障时发现很多挂载失败的报警本质上都是CSI组件不健康最后在CSI日志里找到是认证超时。4.3 权限与UID/GID问题PVC挂上了Pod却写不进去比挂载失败更磨人的是挂载成功但Pod里没写入权限。这类问题的根源在于CephFS的权限模型和K8s的fsGroup机制相互作用。默认情况下通过root挂载的CephFS目录权限都是root:root而容器里的进程如果以非root用户运行比如nginx的www-data用户、Java应用的app用户对这个目录就没有写权限。解决办法有三个修改PVC对应的子卷权限在Ceph侧手动chown把子卷目录属主改成目标UID/GID。利用K8s的fsGroup在Pod的securityContext配置fsGroup让Kubelet自动把挂载点属组改成这个GID。用CSI的uid/gid参数CephCSI支持在StorageClass里通过csi.storage.k8s.io/pvc.name或者volumeAttributes里的uid、gid指定挂载后目录的属主和属组。我的实践建议是第二种fsGroup最贴近K8s原生语义也不需要在Ceph侧手工操作。配置示例apiVersion: apps/v1 kind: Deployment spec: template: spec: securityContext: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch containers: - name: app securityContext: runAsUser: 1000fsGroupChangePolicy: OnRootMismatch可以避免每次Pod启动都去递归改属组省掉大量元数据IO。这个字段在CephFS这类大目录文件系统上收益特别明显。还有一点要注意CephFS子卷默认挂载后根目录是subvolume目录本身PVC的容量配额和这个目录实际可写入大小直接相关。如果你给子卷设了配额而PVC申请的storage大小和配额不匹配可能出现PVC显示已创建但写入到一定量就报No space left的情况。4.4 卸载与重挂环境变脏时的正确重置姿势排查过程中你可能需要反复卸载、重挂PVC。这里有个实操经验K8s里卸载不干净会留下僵尸挂载点导致后续所有Pod调度的挂载阶段卡死。遇到Pod一直ContainerCreatingkubelet日志里报MountVolume.MountDevice failed for volume ...但又查不出具体错误时十有八九是残留挂载点。处理方法是找到失败的Pod所在的节点kubectl get pod -A -o wide | grep pod名登录那个节点查看残留挂载点mount | grep ceph df -h | grep ceph找到残留在/var/lib/kubelet/pods/下的挂载路径手动卸载umount -f /var/lib/kubelet/pods/pod-id/volumes/kubernetes.io~csi/...如果umount失败检查是否有进程占用了挂载点fuser -km /var/lib/kubelet/pods/pod-id/volumes/kubernetes.io~csi/...我在多个集群里遇到过这样的问题最终都是这个套路解决的。核心逻辑是K8s的CSI卸载流程是串行的一旦有孤儿挂载点残留新Pod在同节点上挂载就会一直等待锁。所以排障过程中不要随意删Pod先确认挂载点已经清理干净再重新调度。5. 生产环境必须关注的几个进阶细节5.1 子卷配额、快照和回收策略别让存储失控动态供给创建PVC之后ceph-csi默认会在CephFS里创建对应的子卷。但配额不一定准确。我生产集群的做法是给每个业务子卷组设置明确的配额策略ceph fs subvolume quota set myfs vol_ai 100G --group_name group_k8s配额设置完以后这层限制在Ceph侧是强制的即使PVC的storage声明写的是200G但子卷配额只有100G那写到100G就会报错。反过来也一样如果PVC声明10G而子卷配额是100G监控面板上看容量时容易误判。建议以PVC声明为主让子卷配额和PVC声明保持同步。快照的话CephFS本身支持ceph fs subvolume snapshot create但K8s侧的卷快照功能是通过VolumeSnapshotCRD触发的。创建快照时PVC里数据的一致性完全取决于应用本身是否支持崩溃一致性比如数据库类应用快照前还是要停写或者刷盘这个和任何存储都一样。ReclaimPolicy方面我建议生产环境设成Retain而不是Delete。原因很简单动态PV一旦删除PVCDelete策略会直接把CephFS子卷删掉想恢复都没有途径。Retain策略至少给你留一个手动恢复的窗口期。5.2 性能观察kernel mounter vs fuse mounter的真实差距前文已经说过我倾向kernel mounter这里补一个实测概念。同一套Ceph集群、同一批OSD、同一个PVC分别用kernel和fuse挂载后做fio测试结果大致是这样的具体数字因集群而异我只说量级感受场景kernel mounterfuse mounter差距顺序写基准约为kernel的70%明显顺序读基准约为kernel的80%中等4K随机写基准约为kernel的55%明显元数据密集操作基准约为kernel的40%明显FUSE因为要走用户态和内核态切换性能损耗是物理规律不是调参能完全弥补的。所以对性能敏感的训练任务、视频渲染这类IO密集型应用务必使用kernel mounter。另外补充一个kernel mounter的坑**节点内核版本低于4.15时CephFS的内核客户端不支持fsc特性而ceph-csi在某些版本里默认开fsc会导致挂载失败。**如果你在用老内核要么升内核要么在StorageClass里显式加fsc: false参数新版本里是disablefsc: true。5.3 跨命名空间共享与多个子卷组的规划最后聊聊共享和隔离的取舍。CephFS天然支持RWX但K8s的PVC默认是命名空间级别的资源。跨命名空间共享数据有几种常见做法方案A每个命名空间创建自己的PVC但VolumeHandle指向同一个CephFS子卷。优点是每个命名空间都有独立的PV/PVC对象权限控制清晰缺点是两个PVC相当于挂载了同一个目录如果你在应用层对目录写权限没有约束还是能互相看到数据。方案B一个PVC挂到多个Pod同一命名空间不同命名空间通过CephFS的目录权限划分。这种适合内部平台型组件比如统一的日志系统。方案C子卷组按环境分。比如group_dev、group_prod通过Ceph侧授权把不同客户端账号锁在不同的子卷组里这样就算K8s集群被渗透一个账号也只影响一个子卷组。我现在的习惯是首选方案C配合方案AK8s里每个PVC绑定一个独立的CephFS子卷同时通过子卷组做环境隔离客户端账号按环境分别创建。这样既享受了共享文件系统的灵活性又把故障爆炸半径控制在一个子卷组内。5.4 节点宕机、Pod漂移和CephFS挂载点清理的联动生产环境跑久了你一定会遇到节点宕机后Pod被重新调度到另一台节点的情况。这里有个细节旧节点如果一直没恢复旧节点上的CephFS挂载点不会自动清理但只要把Pod删掉并等待volume detach超时K8s最终会强制解绑。这个等待时间由attach-detach-reconcile-sync-period控制默认几分钟。如果业务无法接受这么久的等待可以调短kube-controller-manager的--attach-detach-reconcile-sync-period参数但代价是集群控制面更频繁地去扫描卷量大的时候会给APIServer带来额外压力。这里又是一个取舍我建议先用默认值等真遇到问题了再按实际情况调。提示无论什么方案CephFS挂载涉及的所有变更先在测试集群完整走一遍再上生产。特别是StorageClass参数、mounter切换这类全局性的改动会影响集群里所有PVC谨慎不过分。最后再说点实在的我个人在实际操作中的体会是cephfs挂载到k8s这件事难点从来不在执行那几步命令而在于理解每一层组件的职责边界Ceph侧管的是文件系统、子卷、权限K8s侧管的是Secret、PV/PVC、StorageClass的编排CSI插件管的是两边的翻译。三者的日志要分开看问题就能很快定位到某一层。再分享一个小技巧每次搭建完先创建一个测试PVC并挂到一个临时Pod里写一个带时间戳的文件再读出来然后删掉Pod观察PVC释放状态最后确认Ceph侧子卷是否按预期保留或删除。这套流程走通一遍比看十篇文档都有用。后面如果再遇到挂载问题你至少能确定是自己环境的问题还是配置的问题排查范围能缩短一大半。
返回列表