这个系列写到第7篇,终于轮到K8s资源对象里最容易把人绕晕的一块了。Pod、Deployment、Service、StatefulSet、DaemonSet、Volume、PV、PVC,每个名词单独看都能找到一堆文档,可真到要把它们串起来、并且接入Ceph这种外置分布式存储的时候,很多人就卡住了。问题往往不在某个具体资源对象本身,而在于搞不清楚它们之间的职责边界和调用关系。这篇文章把资源对象按“工作负载、服务接入、存储对接”三条线重新梳理一遍,配合Ceph RBD做持久化存储的完整链路,最后给出一套可以直接参考的StatefulSet示例。适合已经跑通基础Pod和Deployment、正准备接触有状态服务和持久化存储的同学,也可以当排障时的速查手册用。顺序上我会从K8s的API设计讲起,再从探针和Service切入服务治理,接着拆解StatefulSet和DaemonSet这两类特殊工作负载,最后落到Volume/PV/PVC与Ceph的实际对接。1. 理解K8s资源对象的设计逻辑1.1 一切操作最后都是对着API Server说话很多人用kubectl习惯了,以为kubectl就是K8s本体。实际上kubectl只是一个客户端,它做的事情就是把你的操作翻译成HTTP请求,发给集群的API Server。API Server是唯一的入口,所有资源对象的创建、更新、删除、查询,最终都要经过它。这里有个关键认知:K8s并不是“命令式”地告诉你“我要在这个节点上运行一个容器”,而是“声明式”地描述“我希望集群里存在一个怎样的状态”。比如你写了一个Deployment清单,说“我要3个nginx副本”,API Server会把这个期望状态存进etcd,然后Deployment控制器、ReplicaSet控制器会不断观察实际状态,发现副本数不够就去创建Pod,多了就去回收。整个系统就像是一个永不疲倦的“对账机器人”,始终在拉近期望状态和实际状态之间的距离。生活里可以类比成酒店订房:你(用户)提交订单(期望状态),前台(API Server)记录订单,客房部(控制器)看到订单就去打扫房间、布置物品,直到客房状态和订单完全匹配。你不需要亲自去房间摆东西,只需要把“我要什么”写清楚。所以学习资源对象时,一定要养成一个习惯:凡是能用kubectl apply提交YAML的,尽量用YAML表达,不要每次都kubectl run一把梭。声明式配置有几个实实在在的好处:可评审、可回滚、可复用,而且出错时你能从etcd里的期望状态反推出设计意图。1.2 资源对象的分类与依赖关系K8s资源对象乍一看很多,但归类之后其实非常清晰。按使用场景大致可以分成五类:分类典型资源对象核心作用工作负载类Pod、Deployment、StatefulSet、DaemonSet、Job决定容器怎么跑、跑几个、跑在哪服务接入类Service、Endpoint、Ingress决定流量怎么进、怎么分存储类Volume、PV、PVC、StorageClass决定数据放哪、怎么申请、怎么回收配置类ConfigMap、Secret把配置和镜像解耦,镜像可以不变,配置变了元信息与权限类Namespace、Label、RBAC决定隔离、标识、谁能操作什么这些类别之间不是孤立的,而是层层依赖的关系。一个典型链路是这样的:Deployment创建Pod,Pod通过label被Service选中,Service把流量转发到Pod的端口;同时Pod声明了volumeMount,挂载了一个PVC,PVC通过StorageClass动态创建PV,PV底层对接Ceph RBD块设备。这条链路其实就是一整个应用的“骨架”:工作负载是肌肉,Service是血管,PV/PVC是存储仓库,Ceph是仓库管理员。刚开始接触时,我强烈建议用kubectl api-resources看一下当前集群支持的资源列表,再配合kubectl explain <资源类型>查看字段说明。你不需要背字段,但要知道每个资源的用途、apiVersion、以及它跟其他资源是怎么关联的。这比你背十篇操作命令都管用。1.3 Ceph在K8s里的角色定位标题里把Ceph和K8s资源对象放在一起,是因为K8s本身不存数据——它只负责调度容器,数据要落到持久化存储里。Ceph就是目前最主流的开源分布式存储之一,提供三种访问接口:RBD(块存储)、CephFS(文件存储)、RGW(对象存储)。在K8s场景里,最常用的是RBD,因为它性能好、延迟低,适合数据库这类需要块设备的应用。需要特别理解的一点是:K8s不会直接“连”Ceph,而是通过CSI(Container Storage Interface)插件来对接。CSI可以把不同存储厂商的驱动能力统一暴露给K8s,让K8s像操作本地存储一样操作远端存储。你只需要创建StorageClass,指定provisioner为rbd.csi.ceph.com,再填上Ceph集群的monitor地址、pool名称、认证用户,就能实现PVC创建时自动在Ceph上分配块设备,PVC删除时自动回收。把这个机制想明白后,后面看任何存储资源对象都会轻松很多。PV/PVC/StorageClass本质上就是一层“介于K8s和存储系统之间的抽象”,它们存在的意义是让应用开发者不关心底层存储细节,让存储管理员不关心应用部署位置。2. 探针与Service:把应用健康和流量入口理清楚2.1 三种探针到底分别管什么Pod探针是K8s对应用做健康检查的主要手段。很多新手一上来就只配livenessProbe,以为“能探测就是健康”,结果线上频繁误杀重启,其实是因为没搞懂三种探针各自的定位。startupProbe(启动探针):管“启动期”。有些应用初始化特别慢,比如要加载模型、连接数据库、预热缓存,可能要一两分钟才能对外提供服务。如果不用startupProbe,livenessProbe可能启动后几秒就开始探测,连续失败几次就把容器杀了。startupProbe成功之前,其他探针都不会执行,这相当于给了慢启动应用一个“保护期”。livenessProbe(存活探针):管“活着没”。探测失败,K8s会杀掉容器并按restartPolicy重启。它回答的问题是“这个容器还健不健康,要不要救一下”。readinessProbe(就绪探针):管“能不能接流量”。探测失败时容器不会被杀,而是从Service的端点列表里摘掉,暂时不给它派流量。它回答的问题是“这个容器现在能不能干活”。可以这样记:startupProbe是入职培训期,livenessProbe是定期体检,readinessProbe是红绿灯——红灯亮了车就不放行,但车还停在原地,不会拖去修。探针参数也要理解清楚:initialDelaySeconds表示容器启动后等多久再开始探测,periodSeconds表示探测间隔,timeoutSeconds表示单次探测超时,failureThreshold表示连续失败多少次才判定失败,successThreshold表示连续成功多少次才判定成功。下面是一个用HTTP Get方式做健康检查的完整例子:apiVersion: v1 kind: Pod metadata: name: demo-app labels: app: demo-app spec: containers: - name: app image: demo-app:1.0.0 ports: - containerPort: 8080 startupProbe: httpGet: path: /health/startup port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1这里有个实战体会:探针的探测方法有三种,httpGet、tcpSocket、exec。TCP探测只能说明端口能连上,不代表业务真的可用;HTTP探测更接近用户视角,因为服务端要返回正确的状态码才判成功。exec适合容器里没有HTTP服务只有脚本的场景,比如用cat某个临时文件是否存在来判断进程是否初始化完成。要特别注意一个容易踩的坑:不要在livenessProbe里依赖那些可能因负载波动而暂时变慢的接口。比如一个接口平时10毫秒返回,高峰期要2秒,你timeoutSeconds设成1秒,那高峰期就会疯狂误杀。探针的timeoutSeconds不能太紧,一般建议至少3秒,而且要配合CPU/内存的limits一起看——如果容器被限流,探针也很容易超时。2.2 Service的四种类型怎么选Service是K8s做服务发现和负载均衡的抽象层。Pod的IP是会漂移的,每次重启都可能变,但Service的IP和DNS名字是稳定的。应用通过Service访问后端Pod,不需要关心Pod到底跑在哪里。Service主要有四种类型:ClusterIP:默认类型,只能在集群内部访问,分配一个虚拟IP。适合内部服务之间的调用,比如后端API访问数据库。NodePort:在每个节点上开一个端口(默认30000-32767),通过节点IP:NodePort访问。适合测试环境或者不想上负载均衡器的场景。LoadBalancer:在云环境里,会自动创建云厂商的负载均衡器,公网IP挂到Service上,适合对外暴露HTTP/HTTPS服务。ExternalName:把Service解析到外部域名,相当于给外部的某个域名起了一个集群内的别名。一个Service最基本的配置是selector、port和targetPort三件套:apiVersion: v1 kind: Service metadata: name: demo-svc spec: type: ClusterIP selector: app: demo-app ports: - name: http port: 80 targetPort: 8080 protocol: TCP这里port是Service对外暴露的端口,targetPort是Pod里容器真正监听的端口。如果你给容器端口起了名字,比如containerPort叫http,targetPort可以直接写http,这样就算容器端口换了,Service配置不用改。还有一类特殊的Service叫Headless Service,spec里设置clusterIP: None,不分配虚拟IP,DNS解析直接返回所有后端Pod的IP列表。这东西配合StatefulSet特别有用,因为StatefulSet的每个Pod都有稳定的网络标识,比如web-0, web-1,通过Headless Service可以实现“每个Pod都有独立域名”的效果,比如web-0.default.svc.cluster.local。数据库、消息队列这种需要节点间互相直连的应用,几乎都会用到Headless Service。3. StatefulSet与DaemonSet:两类特殊工作负载的适用边界3.1 DaemonSet:集群级守护进程的首选DaemonSet从名字就能猜到,它要保证在集群的每个符合条件的节点上运行一个Pod副本。节点加入集群就自动运行,节点销毁就自动清理。最典型的场景是日志采集、监控Agent、网络插件、存储插件,比如每台节点上都要部署一个日志采集器,把/var/log下的日志收集起来,或者每台节点上都要部署一个网络代理,负责容器网络的配置。DaemonSet和Deployment的关键区别在于:Deployment关心的是“集群里总共有多少个副本”,它可以集中跑在某一台节点上;DaemonSet关心的是“每个节点上都要有”,它是按节点维度来调度的。如果一个集群有10个节点,DaemonSet的副本数就是10,而且每个节点最多一个。DaemonSet的滚动更新方式也和Deployment类似,支持RollingUpdate和OnDelete两种策略。RollingUpdate是逐个节点更新,默认maxUnavailable设置为1,保证不会一次性把整批Agent都停掉。OnDelete则适合你自己手动控制更新节奏的场景。一个比较常见的坑是DaemonSet想跑在控制平面节点上。K8s的Master节点默认有NoSchedule污点,普通Pod不会被调度上去,DaemonSet默认也一样。如果确实需要在Master节点上跑Agent,要加上对应的tolerations。我在实际部署日志采集器时就有过这样的经历:Agent在Worker节点都正常,但Master节点的日志一直缺失,查了半天才发现是没加容忍配置。3.2 StatefulSet:身份、拓扑与存储都要稳定StatefulSet是K8s里有状态应用的核心工作负载。它和Deployment最大的区别在于三件事:稳定的网络标识、稳定的存储绑定、有序的部署和伸缩。先看网络标识。StatefulSet创建的Pod名字是固定的,比如stateful-name-0、stateful-name-1,按序号递增。Pod被删除重建后,名字和主机名都不变,这就意味着它对应的DNS域名也不变。配合Headless Service,每个Pod都可以通过podName.serviceName.namespace.svc.cluster.local这样的域名被访问。对于需要节点间互相发现和组网的分布式系统来说,这个能力至关重要。再看存储绑定。StatefulSet支持volumeClaimTemplates,相当于“PVC模板”。每次创建Pod时,按模板自动生成一个PVC,命名规则是<volumeClaimTemplateName>-<podName>。Pod被重建后,它会重新绑定原来的PVC,保证数据不丢。比如某个Pod的PVC叫data-web-0,就算Pod被删了,data-web-0还会留在集群里,等新的web-0创建时,直接重新挂载这个PVC,数据原封不动。最后是有序部署和伸缩。StatefulSet在扩容时按序号从0到N-1依次创建,每个Pod必须启动并进入Ready状态后,才会创建下一个Pod。缩容时则相反,从序号最大的Pod开始删。如果某个Pod一直不Ready,后续的Pod也不会启动,避免出现集群脑裂或数据不一致。正因为这些特性,StatefulSet几乎成了数据库、消息队列、ZooKeeper、etcd这类有状态服务的唯一正确打开方式。Deployment适合无状态应用,Pod随便换、IP随便漂,数据无处可存;StatefulSet适合“我就是要固定身份、固定存储、按顺序来”的应用。3.3 有状态服务如何与Service、存储结合StatefulSet和Service、存储之间是“三件套”关系。一个标准的有状态服务部署,通常由三部分组成:Headless Service负责提供稳定的DNS入口、StatefulSet负责管理Pod、PVC/StorageClass负责提供持久化存储。以部署一个3节点的分布式存储型应用为例:Headless Service给每个Pod提供podName.svcNamespace.svc.cluster.local的域名,Pod之间通过这个域名互相通信;StatefulSet通过volumeClaimTemplates为每个Pod创建独立的PVC,确保每个Pod的数据互不干扰;底层StorageClass指向Ceph RBD,每次PVC创建时自动在Ceph上分配一块RBD块设备。这里有一个容易被忽略的细节:StatefulSet的Pod在滚动更新时是“倒序更新”的,先更新序号最大的Pod,逐步往前。RollingUpdate时如果某个Pod更新失败,后续Pod不会继续更新。这个行为对有状态应用很重要,因为很多有状态应用要求不能“同时”更新多个副本,否则可能出现新旧版本混乱导致数据不一致。另一个常见的认知误区是:删除StatefulSet并不会删除Pod,也不会删除PVC。kubectl delete statefulset会把StatefulSet管理下的Pod一起删掉(除非用--cascade=orphan),但PVC一定会留下。这是K8s故意设计的防护机制——防止你一条命令把数据库数据全删了。很多人在测试环境删StatefulSet后,发现PVC还在,以为出问题了,其实这是保护行为。4. Volume/PV/PVC如何与Ceph RBD真正对接4.1 Volume、PV、PVC、StorageClass四层关系提到存储,K8s里Volume是最基础的抽象。它有很多种类型:emptyDir、hostPath、configMap、secret、pvc等等。emptyDir是Pod级别的临时目录,Pod删除就没了;hostPath是把宿主机目录挂载进容器,能用但会很危险,因为Pod被调度到别的节点时数据就不通了。真正解决持久化问题的,是后面三个东西的组合:PV、PVC、StorageClass。三者关系可以这样理解:PV是集群层面的存储资源,相当于“仓库里的一块硬盘”。PV必须由存储管理员预先创建,或者由StorageClass动态创建。PVC是应用层面的存储请求,相当于“我要申请一块100GB的硬盘”。PVC描述的是“我要什么”,不关心底层是Ceph还是本地磁盘。StorageClass是动态供给的“工厂”。PVC创建后,如果指定了storageClassName,对应的provisioner就会自动去底层存储系统创建PV,再绑定到PVC。PV的生命周期有四个状态:Available(空闲)、Bound(被PVC绑定)、Released(PVC被删但PV还没回收)、Failed(回收失败)。静态供给的时候,PV和PVC的绑定条件包括存储大小、访问模式(比如ReadWriteOnce)、storageClassName是否匹配。动态供给则不需要预先创建PV,一切都由StorageClass自动完成。AccessMode是经常被搞混的一个点:ReadWriteOnce(RWO)表示只能被单个节点挂载,ReadWriteMany(RWX)表示可以被多个节点同时挂载,ReadOnlyMany(ROX)表示多个节点只读挂载。这里“Once”和“Many”说的都是节点维度,不是Pod维度。比如RBD块存储天然只支持RWO,如果你要多个Pod同时读写同一块存储,就不能用RBD,得用CephFS或其他支持RWX的存储。4.2 Ceph侧准备:pool、用户、keyring要从K8s动态创建Ceph RBD块设备,Ceph侧必须准备好三样东西:一个存储池(pool)、一个认证用户、一份keyring密钥。先创建存储池。Ceph的pool是逻辑上的存储空间划分,RBD块设备都要放在某个pool里。创建命令大概是ceph osd pool create kubernetes 128,后面的128是PG(Placement Group)数量,具体值要根据OSD数量和数据量来定,一般每个OSD建议分配50到100个PG。PG数设太小会导致数据分布不均匀,设太大又增加开销。再创建认证用户。Ceph默认启用cephx认证,K8s的CSI插件连接Ceph时必须使用有效身份。创建一个专门给K8s用的用户,并授权它访问刚才创建的pool:ceph auth get-or-create client.kubernetes mon 'allow r' osd 'allow rwx pool=kubernetes' -o /etc/ceph/ceph.client.kubernetes.keyring这条命令的意思是:创建一个名为client.kubernetes的用户,允许它从monitor读取信息,允许对kubernetes这个pool进行读写。生成的keyring文件里包含用户ID和密钥,后面要放到K8s的Secret里。然后就是部署ceph-csi插件。现在新版K8s都推荐使用ceph-csi,老一代的rbd-provisioner已经在K8s 1.13之后逐步废弃了。ceph-csi由Controller和Node两个组件组成,Controller是Deployment,负责PV创建和删除;Node是DaemonSet,负责块设备的挂载和卸载。这部分部署完成后,K8s里才能识别rbd.csi.ceph.com这个provisioner。4.3 动态供给完整链路示例假设Ceph集群和ceph-csi都准备好了,接下来就是K8s侧的配置。第一步是创建Secret,保存CEPH的认证信息:apiVersion: v1 kind: Secret metadata: name: csi-rbd-secret namespace: kube-system stringData: userID: kubernetes userKey: AQDnxxxxxxxxxxxxxxxxxxxx注意:Secret的namespace要跟ceph-csi插件本身所在的namespace保持一致,否则CSI组件读取不到认证信息。userID和userKey就来自刚才创建的ceph.client.kubernetes.keyring。然后是创建StorageClass:apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: ceph-rbd-sc provisioner: rbd.csi.ceph.com parameters: clusterID: 6c7a8c42-xxxx-xxxx-xxxx-xxxxxxxxxxxx pool: kubernetes csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret csi.storage.k8s.io/provisioner-secret-namespace: kube-system csi.storage.k8s.io/controller-expand-secret-name: csi-rbd-secret csi.storage.k8s.io/controller-expand-secret-namespace: kube-system csi.storage.k8s.io/node-stage-secret-name: csi-rbd-secret csi.storage.k8s.io/node-stage-secret-namespace: kube-system reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true这里的参数有几个值得注意:clusterID是Ceph集群的fsid,通过ceph fsid命令查;pool必须是Ceph里真实存在的pool;reclaimPolicy设为Delete表示PVC删除时PV和底层RBD块设备一起删除,设为Retain则表示保留,用于数据归档;volumeBindingMode设为WaitForFirstConsumer可以让PV在Pod被调度到节点后再创建,这样可以结合节点拓扑避免跨可用区分配块设备。有了StorageClass,PVC就可以动态创建了:apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mydata spec: accessModes: - ReadWriteOnce storageClassName: ceph-rbd-sc resources: requests: storage: 10Gi创建后,kubectl get pvc看状态应该是Bound,kubectl get pv会看到一个由ceph-csi动态创建的PV。此时在Pod里声明volumeMount和volumes,引用这个PVC,就可以把10GB的Ceph RBD块设备挂载到容器里了。这套链路的关键点在于:应用开发者只需要关心PVC的申请和使用,存储管理员只需要关心Ceph pool和StorageClass的配置,两边通过PVC和storageClassName这两个字段解耦。你要是一周前还分不清PV和PVC的区别,现在应该能明白:PVC是需求单,PV是采购回来的硬盘,StorageClass是自动采购系统,Ceph是仓库。5. 常见问题与排查实录5.1 PVC一直Pending,卡在动态供给环节这应该是存储接入时遇到最多的问题,没有之一。PVC状态一直Pending,说明K8s尝试调用StorageClass的provisioner去创建PV,但没成功。排查步骤我建议从下往上查:先kubectl describe pvc <名称>看PVC的事件列表。事件里会明文写清楚是provisioner不认识、Secret缺失、还是连接存储失败。检查StorageClass的provisioner名字是否和ceph-csi部署的插件匹配。一个最常见的翻车现场:安装的ceph-csi版本是v3.x,但StorageClass里的provisioner写成了rbd.csi.ceph.com的旧拼法。检查Secret的namespace和名称。ceph-csi在kube-system下部署,如果Secret放在default namespace,CSI根本找不到。检查Ceph侧的pool是否存在、monitor地址是否可达、认证用户是否有权限。可以用rbd ls -p kubernetes从Ceph侧直接验证。我之前碰到一次很隐蔽的问题:Ceph集群的fsid不对,StorageClass里的clusterID填成了集群名字,导致CSI无法定位集群。这个字段不是“名字”,是fsid,必须用ceph fsid查出来的UUID。5.2 Pod反复重启,探针配置背锅应用日志明明没有异常,但Pod一直重启,kubectl get pod显示CrashLoopBackOff,首先看kubectl describe pod里的Last State,如果是“Error”或“OOMKilled”,那大概率不是探针问题,是应用本身或者内存限制的问题。如果Last State显示容器是因为探针失败被杀掉的,那就要审视探针配置了。常见问题有三个:一是startupProbe没配,慢启动应用被livenessProbe反复误杀;二是livenessProbe探测的接口本身不稳定,高峰期稍微慢一点就超时;三是readinessProbe和livenessProbe共用同一个接口,就绪失败会导致流量摘除,但liveness失败会直接重启,两种后果完全不一样。给探针调参时,我会先观察应用启动到Ready需要多长时间,再观察高峰期接口响应时间,然后留出2倍的冗余。failureThreshold也不要设太小,至少3次,避免网络抖动触发误杀。5.3 DaemonSet没有调度到某些节点DaemonSet调度不上去,绝大多数情况是节点污点和容忍度的问题。先kubectl describe node看节点有哪些Taints,再看DaemonSet是否在Pod模板里定义了匹配的tolerations。如果某节点有node-role.kubernetes.io/control-plane:NoSchedule这样的污点,普通Pod和DaemonSet默认都不会调度上去。还有一个容易被忽略的点:如果DaemonSet设置了nodeSelector或nodeAffinity,而目标节点没有匹配的label,Pod也会卡在Pending。排查时用kubectl get pods -o wide -n kube-system看具体Pod卡在哪个节点,再用kubectl describe ds <名称>看事件,事件里会把调度失败的原因写得很清楚。5.4 StatefulSet的PVC和Pod重建后数据表现如果你删掉一个StatefulSet管理的Pod,等它重新创建,PVC会重新绑定,数据还在,这是正常的。但如果你连PVC也一起删了,且StorageClass的reclaimPolicy是Delete,那么底层Ceph块设备也会被删,数据就真没了。如果只是想“重置一下应用”,但想保留数据,千万别回收PVC。另外StatefulSet缩容不会删除对应PVC,扩容时如果希望重用旧的PVC,要保证Pod名字一致。比如原来有web-0到web-2,缩容到web-0后,再扩容到3副本,web-1和web-2会重新绑定原来的PVC,数据还是老数据。这个特性对恢复集群特别有用,但也要注意:如果你的应用本身数据有问题,绑回旧PVC反而会把脏数据带回来。存储数据安全这块我再多说一句:Dynamic Provisioning虽然省事,但Delete策略意味着你在删除PVC时要格外谨慎。生产环境我倾向把reclaimPolicy设为Retain,通过定期任务主动备份和清理,避免一条误删命令把底层存储数据全清掉。5.5 常见问题速查表现象常见原因排查手段PVC一直PendingStorageClass provisioner不匹配 / Secret缺失 / pool不存在kubectl describe pvc查看事件Pod反复CrashLoopBackOff探针误杀 / 应用启动失败 / OOMkubectl describe pod查看Last StateService无法访问selector不匹配 / targetPort写错 / Pod未Readykubectl get endpoints查看EndpointsDaemonSet部分节点没PodTaints未容忍 / nodeSelector不匹配kubectl describe node / dsStatefulSet Pod启动卡在PendingPVC无法绑定 / 有序启动等待前置Podkubectl describe pod查看事件Ceph认证失败Secret namespace不对 / userKey错误用keyring手动执行rados命令验证这几个月里我把K8s资源对象从API到存储整体过了一遍之后,最大的感触是:K8s的设计其实就是在“解耦”两个字上反复做文章。工作量负载和网络解耦,于是有了Service;应用和数据解耦,于是有了PV/PVC/StorageClass;存储实现和存储接口解耦,于是有了CSI;有状态和无状态解耦,于是有了StatefulSet和Deployment的区分。你只要抓住每个资源对象“它到底在解决哪两个组件之间的解耦问题”,就再也不会觉得K8s是一堆名词的堆砌了。最后再分享一个个人习惯:碰到不懂的资源对象,不要急着去网上找“模板”,先kubectl explain <资源类型>看官方字段说明,再kubectl get <资源> -o yaml看一个真实存在的对象是怎么写的。这两个命令配合起来比看十篇教程都管用。Ceph接入也一样,先把pool、user、keyring这三样基础搞定,再往StorageClass上怼参数,问题会少一大半。
这个系列写到第7篇,终于轮到K8s资源对象里最容易把人绕晕的一块了。Pod、Deployment、Service、StatefulSet、DaemonSet、Volume、PV、PVC,每个名词单独看都能找到一堆文档,可真到要把它们串起来、并且接入Ceph这种外置分布式存储的时候,很多人就卡住了。问题往往不在某个具体资源对象本身,而在于搞不清楚它们之间的职责边界和调用关系。这篇文章把资源对象按“工作负载、服务接入、存储对接”三条线重新梳理一遍,配合Ceph RBD做持久化存储的完整链路,最后给出一套可以直接参考的StatefulSet示例。适合已经跑通基础Pod和Deployment、正准备接触有状态服务和持久化存储的同学,也可以当排障时的速查手册用。顺序上我会从K8s的API设计讲起,再从探针和Service切入服务治理,接着拆解StatefulSet和DaemonSet这两类特殊工作负载,最后落到Volume/PV/PVC与Ceph的实际对接。1. 理解K8s资源对象的设计逻辑1.1 一切操作最后都是对着API Server说话很多人用kubectl习惯了,以为kubectl就是K8s本体。实际上kubectl只是一个客户端,它做的事情就是把你的操作翻译成HTTP请求,发给集群的API Server。API Server是唯一的入口,所有资源对象的创建、更新、删除、查询,最终都要经过它。这里有个关键认知:K8s并不是“命令式”地告诉你“我要在这个节点上运行一个容器”,而是“声明式”地描述“我希望集群里存在一个怎样的状态”。比如你写了一个Deployment清单,说“我要3个nginx副本”,API Server会把这个期望状态存进etcd,然后Deployment控制器、ReplicaSet控制器会不断观察实际状态,发现副本数不够就去创建Pod,多了就去回收。整个系统就像是一个永不疲倦的“对账机器人”,始终在拉近期望状态和实际状态之间的距离。生活里可以类比成酒店订房:你(用户)提交订单(期望状态),前台(API Server)记录订单,客房部(控制器)看到订单就去打扫房间、布置物品,直到客房状态和订单完全匹配。你不需要亲自去房间摆东西,只需要把“我要什么”写清楚。所以学习资源对象时,一定要养成一个习惯:凡是能用kubectl apply提交YAML的,尽量用YAML表达,不要每次都kubectl run一把梭。声明式配置有几个实实在在的好处:可评审、可回滚、可复用,而且出错时你能从etcd里的期望状态反推出设计意图。1.2 资源对象的分类与依赖关系K8s资源对象乍一看很多,但归类之后其实非常清晰。按使用场景大致可以分成五类:分类典型资源对象核心作用工作负载类Pod、Deployment、StatefulSet、DaemonSet、Job决定容器怎么跑、跑几个、跑在哪服务接入类Service、Endpoint、Ingress决定流量怎么进、怎么分存储类Volume、PV、PVC、StorageClass决定数据放哪、怎么申请、怎么回收配置类ConfigMap、Secret把配置和镜像解耦,镜像可以不变,配置变了元信息与权限类Namespace、Label、RBAC决定隔离、标识、谁能操作什么这些类别之间不是孤立的,而是层层依赖的关系。一个典型链路是这样的:Deployment创建Pod,Pod通过label被Service选中,Service把流量转发到Pod的端口;同时Pod声明了volumeMount,挂载了一个PVC,PVC通过StorageClass动态创建PV,PV底层对接Ceph RBD块设备。这条链路其实就是一整个应用的“骨架”:工作负载是肌肉,Service是血管,PV/PVC是存储仓库,Ceph是仓库管理员。刚开始接触时,我强烈建议用kubectl api-resources看一下当前集群支持的资源列表,再配合kubectl explain <资源类型>查看字段说明。你不需要背字段,但要知道每个资源的用途、apiVersion、以及它跟其他资源是怎么关联的。这比你背十篇操作命令都管用。1.3 Ceph在K8s里的角色定位标题里把Ceph和K8s资源对象放在一起,是因为K8s本身不存数据——它只负责调度容器,数据要落到持久化存储里。Ceph就是目前最主流的开源分布式存储之一,提供三种访问接口:RBD(块存储)、CephFS(文件存储)、RGW(对象存储)。在K8s场景里,最常用的是RBD,因为它性能好、延迟低,适合数据库这类需要块设备的应用。需要特别理解的一点是:K8s不会直接“连”Ceph,而是通过CSI(Container Storage Interface)插件来对接。CSI可以把不同存储厂商的驱动能力统一暴露给K8s,让K8s像操作本地存储一样操作远端存储。你只需要创建StorageClass,指定provisioner为rbd.csi.ceph.com,再填上Ceph集群的monitor地址、pool名称、认证用户,就能实现PVC创建时自动在Ceph上分配块设备,PVC删除时自动回收。把这个机制想明白后,后面看任何存储资源对象都会轻松很多。PV/PVC/StorageClass本质上就是一层“介于K8s和存储系统之间的抽象”,它们存在的意义是让应用开发者不关心底层存储细节,让存储管理员不关心应用部署位置。2. 探针与Service:把应用健康和流量入口理清楚2.1 三种探针到底分别管什么Pod探针是K8s对应用做健康检查的主要手段。很多新手一上来就只配livenessProbe,以为“能探测就是健康”,结果线上频繁误杀重启,其实是因为没搞懂三种探针各自的定位。startupProbe(启动探针):管“启动期”。有些应用初始化特别慢,比如要加载模型、连接数据库、预热缓存,可能要一两分钟才能对外提供服务。如果不用startupProbe,livenessProbe可能启动后几秒就开始探测,连续失败几次就把容器杀了。startupProbe成功之前,其他探针都不会执行,这相当于给了慢启动应用一个“保护期”。livenessProbe(存活探针):管“活着没”。探测失败,K8s会杀掉容器并按restartPolicy重启。它回答的问题是“这个容器还健不健康,要不要救一下”。readinessProbe(就绪探针):管“能不能接流量”。探测失败时容器不会被杀,而是从Service的端点列表里摘掉,暂时不给它派流量。它回答的问题是“这个容器现在能不能干活”。可以这样记:startupProbe是入职培训期,livenessProbe是定期体检,readinessProbe是红绿灯——红灯亮了车就不放行,但车还停在原地,不会拖去修。探针参数也要理解清楚:initialDelaySeconds表示容器启动后等多久再开始探测,periodSeconds表示探测间隔,timeoutSeconds表示单次探测超时,failureThreshold表示连续失败多少次才判定失败,successThreshold表示连续成功多少次才判定成功。下面是一个用HTTP Get方式做健康检查的完整例子:apiVersion: v1 kind: Pod metadata: name: demo-app labels: app: demo-app spec: containers: - name: app image: demo-app:1.0.0 ports: - containerPort: 8080 startupProbe: httpGet: path: /health/startup port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1这里有个实战体会:探针的探测方法有三种,httpGet、tcpSocket、exec。TCP探测只能说明端口能连上,不代表业务真的可用;HTTP探测更接近用户视角,因为服务端要返回正确的状态码才判成功。exec适合容器里没有HTTP服务只有脚本的场景,比如用cat某个临时文件是否存在来判断进程是否初始化完成。要特别注意一个容易踩的坑:不要在livenessProbe里依赖那些可能因负载波动而暂时变慢的接口。比如一个接口平时10毫秒返回,高峰期要2秒,你timeoutSeconds设成1秒,那高峰期就会疯狂误杀。探针的timeoutSeconds不能太紧,一般建议至少3秒,而且要配合CPU/内存的limits一起看——如果容器被限流,探针也很容易超时。2.2 Service的四种类型怎么选Service是K8s做服务发现和负载均衡的抽象层。Pod的IP是会漂移的,每次重启都可能变,但Service的IP和DNS名字是稳定的。应用通过Service访问后端Pod,不需要关心Pod到底跑在哪里。Service主要有四种类型:ClusterIP:默认类型,只能在集群内部访问,分配一个虚拟IP。适合内部服务之间的调用,比如后端API访问数据库。NodePort:在每个节点上开一个端口(默认30000-32767),通过节点IP:NodePort访问。适合测试环境或者不想上负载均衡器的场景。LoadBalancer:在云环境里,会自动创建云厂商的负载均衡器,公网IP挂到Service上,适合对外暴露HTTP/HTTPS服务。ExternalName:把Service解析到外部域名,相当于给外部的某个域名起了一个集群内的别名。一个Service最基本的配置是selector、port和targetPort三件套:apiVersion: v1 kind: Service metadata: name: demo-svc spec: type: ClusterIP selector: app: demo-app ports: - name: http port: 80 targetPort: 8080 protocol: TCP这里port是Service对外暴露的端口,targetPort是Pod里容器真正监听的端口。如果你给容器端口起了名字,比如containerPort叫http,targetPort可以直接写http,这样就算容器端口换了,Service配置不用改。还有一类特殊的Service叫Headless Service,spec里设置clusterIP: None,不分配虚拟IP,DNS解析直接返回所有后端Pod的IP列表。这东西配合StatefulSet特别有用,因为StatefulSet的每个Pod都有稳定的网络标识,比如web-0, web-1,通过Headless Service可以实现“每个Pod都有独立域名”的效果,比如web-0.default.svc.cluster.local。数据库、消息队列这种需要节点间互相直连的应用,几乎都会用到Headless Service。3. StatefulSet与DaemonSet:两类特殊工作负载的适用边界3.1 DaemonSet:集群级守护进程的首选DaemonSet从名字就能猜到,它要保证在集群的每个符合条件的节点上运行一个Pod副本。节点加入集群就自动运行,节点销毁就自动清理。最典型的场景是日志采集、监控Agent、网络插件、存储插件,比如每台节点上都要部署一个日志采集器,把/var/log下的日志收集起来,或者每台节点上都要部署一个网络代理,负责容器网络的配置。DaemonSet和Deployment的关键区别在于:Deployment关心的是“集群里总共有多少个副本”,它可以集中跑在某一台节点上;DaemonSet关心的是“每个节点上都要有”,它是按节点维度来调度的。如果一个集群有10个节点,DaemonSet的副本数就是10,而且每个节点最多一个。DaemonSet的滚动更新方式也和Deployment类似,支持RollingUpdate和OnDelete两种策略。RollingUpdate是逐个节点更新,默认maxUnavailable设置为1,保证不会一次性把整批Agent都停掉。OnDelete则适合你自己手动控制更新节奏的场景。一个比较常见的坑是DaemonSet想跑在控制平面节点上。K8s的Master节点默认有NoSchedule污点,普通Pod不会被调度上去,DaemonSet默认也一样。如果确实需要在Master节点上跑Agent,要加上对应的tolerations。我在实际部署日志采集器时就有过这样的经历:Agent在Worker节点都正常,但Master节点的日志一直缺失,查了半天才发现是没加容忍配置。3.2 StatefulSet:身份、拓扑与存储都要稳定StatefulSet是K8s里有状态应用的核心工作负载。它和Deployment最大的区别在于三件事:稳定的网络标识、稳定的存储绑定、有序的部署和伸缩。先看网络标识。StatefulSet创建的Pod名字是固定的,比如stateful-name-0、stateful-name-1,按序号递增。Pod被删除重建后,名字和主机名都不变,这就意味着它对应的DNS域名也不变。配合Headless Service,每个Pod都可以通过podName.serviceName.namespace.svc.cluster.local这样的域名被访问。对于需要节点间互相发现和组网的分布式系统来说,这个能力至关重要。再看存储绑定。StatefulSet支持volumeClaimTemplates,相当于“PVC模板”。每次创建Pod时,按模板自动生成一个PVC,命名规则是<volumeClaimTemplateName>-<podName>。Pod被重建后,它会重新绑定原来的PVC,保证数据不丢。比如某个Pod的PVC叫data-web-0,就算Pod被删了,data-web-0还会留在集群里,等新的web-0创建时,直接重新挂载这个PVC,数据原封不动。最后是有序部署和伸缩。StatefulSet在扩容时按序号从0到N-1依次创建,每个Pod必须启动并进入Ready状态后,才会创建下一个Pod。缩容时则相反,从序号最大的Pod开始删。如果某个Pod一直不Ready,后续的Pod也不会启动,避免出现集群脑裂或数据不一致。正因为这些特性,StatefulSet几乎成了数据库、消息队列、ZooKeeper、etcd这类有状态服务的唯一正确打开方式。Deployment适合无状态应用,Pod随便换、IP随便漂,数据无处可存;StatefulSet适合“我就是要固定身份、固定存储、按顺序来”的应用。3.3 有状态服务如何与Service、存储结合StatefulSet和Service、存储之间是“三件套”关系。一个标准的有状态服务部署,通常由三部分组成:Headless Service负责提供稳定的DNS入口、StatefulSet负责管理Pod、PVC/StorageClass负责提供持久化存储。以部署一个3节点的分布式存储型应用为例:Headless Service给每个Pod提供podName.svcNamespace.svc.cluster.local的域名,Pod之间通过这个域名互相通信;StatefulSet通过volumeClaimTemplates为每个Pod创建独立的PVC,确保每个Pod的数据互不干扰;底层StorageClass指向Ceph RBD,每次PVC创建时自动在Ceph上分配一块RBD块设备。这里有一个容易被忽略的细节:StatefulSet的Pod在滚动更新时是“倒序更新”的,先更新序号最大的Pod,逐步往前。RollingUpdate时如果某个Pod更新失败,后续Pod不会继续更新。这个行为对有状态应用很重要,因为很多有状态应用要求不能“同时”更新多个副本,否则可能出现新旧版本混乱导致数据不一致。另一个常见的认知误区是:删除StatefulSet并不会删除Pod,也不会删除PVC。kubectl delete statefulset会把StatefulSet管理下的Pod一起删掉(除非用--cascade=orphan),但PVC一定会留下。这是K8s故意设计的防护机制——防止你一条命令把数据库数据全删了。很多人在测试环境删StatefulSet后,发现PVC还在,以为出问题了,其实这是保护行为。4. Volume/PV/PVC如何与Ceph RBD真正对接4.1 Volume、PV、PVC、StorageClass四层关系提到存储,K8s里Volume是最基础的抽象。它有很多种类型:emptyDir、hostPath、configMap、secret、pvc等等。emptyDir是Pod级别的临时目录,Pod删除就没了;hostPath是把宿主机目录挂载进容器,能用但会很危险,因为Pod被调度到别的节点时数据就不通了。真正解决持久化问题的,是后面三个东西的组合:PV、PVC、StorageClass。三者关系可以这样理解:PV是集群层面的存储资源,相当于“仓库里的一块硬盘”。PV必须由存储管理员预先创建,或者由StorageClass动态创建。PVC是应用层面的存储请求,相当于“我要申请一块100GB的硬盘”。PVC描述的是“我要什么”,不关心底层是Ceph还是本地磁盘。StorageClass是动态供给的“工厂”。PVC创建后,如果指定了storageClassName,对应的provisioner就会自动去底层存储系统创建PV,再绑定到PVC。PV的生命周期有四个状态:Available(空闲)、Bound(被PVC绑定)、Released(PVC被删但PV还没回收)、Failed(回收失败)。静态供给的时候,PV和PVC的绑定条件包括存储大小、访问模式(比如ReadWriteOnce)、storageClassName是否匹配。动态供给则不需要预先创建PV,一切都由StorageClass自动完成。AccessMode是经常被搞混的一个点:ReadWriteOnce(RWO)表示只能被单个节点挂载,ReadWriteMany(RWX)表示可以被多个节点同时挂载,ReadOnlyMany(ROX)表示多个节点只读挂载。这里“Once”和“Many”说的都是节点维度,不是Pod维度。比如RBD块存储天然只支持RWO,如果你要多个Pod同时读写同一块存储,就不能用RBD,得用CephFS或其他支持RWX的存储。4.2 Ceph侧准备:pool、用户、keyring要从K8s动态创建Ceph RBD块设备,Ceph侧必须准备好三样东西:一个存储池(pool)、一个认证用户、一份keyring密钥。先创建存储池。Ceph的pool是逻辑上的存储空间划分,RBD块设备都要放在某个pool里。创建命令大概是ceph osd pool create kubernetes 128,后面的128是PG(Placement Group)数量,具体值要根据OSD数量和数据量来定,一般每个OSD建议分配50到100个PG。PG数设太小会导致数据分布不均匀,设太大又增加开销。再创建认证用户。Ceph默认启用cephx认证,K8s的CSI插件连接Ceph时必须使用有效身份。创建一个专门给K8s用的用户,并授权它访问刚才创建的pool:ceph auth get-or-create client.kubernetes mon 'allow r' osd 'allow rwx pool=kubernetes' -o /etc/ceph/ceph.client.kubernetes.keyring这条命令的意思是:创建一个名为client.kubernetes的用户,允许它从monitor读取信息,允许对kubernetes这个pool进行读写。生成的keyring文件里包含用户ID和密钥,后面要放到K8s的Secret里。然后就是部署ceph-csi插件。现在新版K8s都推荐使用ceph-csi,老一代的rbd-provisioner已经在K8s 1.13之后逐步废弃了。ceph-csi由Controller和Node两个组件组成,Controller是Deployment,负责PV创建和删除;Node是DaemonSet,负责块设备的挂载和卸载。这部分部署完成后,K8s里才能识别rbd.csi.ceph.com这个provisioner。4.3 动态供给完整链路示例假设Ceph集群和ceph-csi都准备好了,接下来就是K8s侧的配置。第一步是创建Secret,保存CEPH的认证信息:apiVersion: v1 kind: Secret metadata: name: csi-rbd-secret namespace: kube-system stringData: userID: kubernetes userKey: AQDnxxxxxxxxxxxxxxxxxxxx注意:Secret的namespace要跟ceph-csi插件本身所在的namespace保持一致,否则CSI组件读取不到认证信息。userID和userKey就来自刚才创建的ceph.client.kubernetes.keyring。然后是创建StorageClass:apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: ceph-rbd-sc provisioner: rbd.csi.ceph.com parameters: clusterID: 6c7a8c42-xxxx-xxxx-xxxx-xxxxxxxxxxxx pool: kubernetes csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret csi.storage.k8s.io/provisioner-secret-namespace: kube-system csi.storage.k8s.io/controller-expand-secret-name: csi-rbd-secret csi.storage.k8s.io/controller-expand-secret-namespace: kube-system csi.storage.k8s.io/node-stage-secret-name: csi-rbd-secret csi.storage.k8s.io/node-stage-secret-namespace: kube-system reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: true这里的参数有几个值得注意:clusterID是Ceph集群的fsid,通过ceph fsid命令查;pool必须是Ceph里真实存在的pool;reclaimPolicy设为Delete表示PVC删除时PV和底层RBD块设备一起删除,设为Retain则表示保留,用于数据归档;volumeBindingMode设为WaitForFirstConsumer可以让PV在Pod被调度到节点后再创建,这样可以结合节点拓扑避免跨可用区分配块设备。有了StorageClass,PVC就可以动态创建了:apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mydata spec: accessModes: - ReadWriteOnce storageClassName: ceph-rbd-sc resources: requests: storage: 10Gi创建后,kubectl get pvc看状态应该是Bound,kubectl get pv会看到一个由ceph-csi动态创建的PV。此时在Pod里声明volumeMount和volumes,引用这个PVC,就可以把10GB的Ceph RBD块设备挂载到容器里了。这套链路的关键点在于:应用开发者只需要关心PVC的申请和使用,存储管理员只需要关心Ceph pool和StorageClass的配置,两边通过PVC和storageClassName这两个字段解耦。你要是一周前还分不清PV和PVC的区别,现在应该能明白:PVC是需求单,PV是采购回来的硬盘,StorageClass是自动采购系统,Ceph是仓库。5. 常见问题与排查实录5.1 PVC一直Pending,卡在动态供给环节这应该是存储接入时遇到最多的问题,没有之一。PVC状态一直Pending,说明K8s尝试调用StorageClass的provisioner去创建PV,但没成功。排查步骤我建议从下往上查:先kubectl describe pvc <名称>看PVC的事件列表。事件里会明文写清楚是provisioner不认识、Secret缺失、还是连接存储失败。检查StorageClass的provisioner名字是否和ceph-csi部署的插件匹配。一个最常见的翻车现场:安装的ceph-csi版本是v3.x,但StorageClass里的provisioner写成了rbd.csi.ceph.com的旧拼法。检查Secret的namespace和名称。ceph-csi在kube-system下部署,如果Secret放在default namespace,CSI根本找不到。检查Ceph侧的pool是否存在、monitor地址是否可达、认证用户是否有权限。可以用rbd ls -p kubernetes从Ceph侧直接验证。我之前碰到一次很隐蔽的问题:Ceph集群的fsid不对,StorageClass里的clusterID填成了集群名字,导致CSI无法定位集群。这个字段不是“名字”,是fsid,必须用ceph fsid查出来的UUID。5.2 Pod反复重启,探针配置背锅应用日志明明没有异常,但Pod一直重启,kubectl get pod显示CrashLoopBackOff,首先看kubectl describe pod里的Last State,如果是“Error”或“OOMKilled”,那大概率不是探针问题,是应用本身或者内存限制的问题。如果Last State显示容器是因为探针失败被杀掉的,那就要审视探针配置了。常见问题有三个:一是startupProbe没配,慢启动应用被livenessProbe反复误杀;二是livenessProbe探测的接口本身不稳定,高峰期稍微慢一点就超时;三是readinessProbe和livenessProbe共用同一个接口,就绪失败会导致流量摘除,但liveness失败会直接重启,两种后果完全不一样。给探针调参时,我会先观察应用启动到Ready需要多长时间,再观察高峰期接口响应时间,然后留出2倍的冗余。failureThreshold也不要设太小,至少3次,避免网络抖动触发误杀。5.3 DaemonSet没有调度到某些节点DaemonSet调度不上去,绝大多数情况是节点污点和容忍度的问题。先kubectl describe node看节点有哪些Taints,再看DaemonSet是否在Pod模板里定义了匹配的tolerations。如果某节点有node-role.kubernetes.io/control-plane:NoSchedule这样的污点,普通Pod和DaemonSet默认都不会调度上去。还有一个容易被忽略的点:如果DaemonSet设置了nodeSelector或nodeAffinity,而目标节点没有匹配的label,Pod也会卡在Pending。排查时用kubectl get pods -o wide -n kube-system看具体Pod卡在哪个节点,再用kubectl describe ds <名称>看事件,事件里会把调度失败的原因写得很清楚。5.4 StatefulSet的PVC和Pod重建后数据表现如果你删掉一个StatefulSet管理的Pod,等它重新创建,PVC会重新绑定,数据还在,这是正常的。但如果你连PVC也一起删了,且StorageClass的reclaimPolicy是Delete,那么底层Ceph块设备也会被删,数据就真没了。如果只是想“重置一下应用”,但想保留数据,千万别回收PVC。另外StatefulSet缩容不会删除对应PVC,扩容时如果希望重用旧的PVC,要保证Pod名字一致。比如原来有web-0到web-2,缩容到web-0后,再扩容到3副本,web-1和web-2会重新绑定原来的PVC,数据还是老数据。这个特性对恢复集群特别有用,但也要注意:如果你的应用本身数据有问题,绑回旧PVC反而会把脏数据带回来。存储数据安全这块我再多说一句:Dynamic Provisioning虽然省事,但Delete策略意味着你在删除PVC时要格外谨慎。生产环境我倾向把reclaimPolicy设为Retain,通过定期任务主动备份和清理,避免一条误删命令把底层存储数据全清掉。5.5 常见问题速查表现象常见原因排查手段PVC一直PendingStorageClass provisioner不匹配 / Secret缺失 / pool不存在kubectl describe pvc查看事件Pod反复CrashLoopBackOff探针误杀 / 应用启动失败 / OOMkubectl describe pod查看Last StateService无法访问selector不匹配 / targetPort写错 / Pod未Readykubectl get endpoints查看EndpointsDaemonSet部分节点没PodTaints未容忍 / nodeSelector不匹配kubectl describe node / dsStatefulSet Pod启动卡在PendingPVC无法绑定 / 有序启动等待前置Podkubectl describe pod查看事件Ceph认证失败Secret namespace不对 / userKey错误用keyring手动执行rados命令验证这几个月里我把K8s资源对象从API到存储整体过了一遍之后,最大的感触是:K8s的设计其实就是在“解耦”两个字上反复做文章。工作量负载和网络解耦,于是有了Service;应用和数据解耦,于是有了PV/PVC/StorageClass;存储实现和存储接口解耦,于是有了CSI;有状态和无状态解耦,于是有了StatefulSet和Deployment的区分。你只要抓住每个资源对象“它到底在解决哪两个组件之间的解耦问题”,就再也不会觉得K8s是一堆名词的堆砌了。最后再分享一个个人习惯:碰到不懂的资源对象,不要急着去网上找“模板”,先kubectl explain <资源类型>看官方字段说明,再kubectl get <资源> -o yaml看一个真实存在的对象是怎么写的。这两个命令配合起来比看十篇教程都管用。Ceph接入也一样,先把pool、user、keyring这三样基础搞定,再往StorageClass上怼参数,问题会少一大半。