
这个系列写到第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创建PodPod通过label被Service选中Service把流量转发到Pod的端口同时Pod声明了volumeMount挂载了一个PVCPVC通过StorageClass动态创建PVPV底层对接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而是通过CSIContainer 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启动探针管“启动期”。有些应用初始化特别慢比如要加载模型、连接数据库、预热缓存可能要一两分钟才能对外提供服务。如果不用startupProbelivenessProbe可能启动后几秒就开始探测连续失败几次就把容器杀了。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叫httptargetPort可以直接写http这样就算容器端口换了Service配置不用改。还有一类特殊的Service叫Headless Servicespec里设置clusterIP: None不分配虚拟IPDNS解析直接返回所有后端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一起删掉除非用--cascadeorphan但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绑定、ReleasedPVC被删但PV还没回收、Failed回收失败。静态供给的时候PV和PVC的绑定条件包括存储大小、访问模式比如ReadWriteOnce、storageClassName是否匹配。动态供给则不需要预先创建PV一切都由StorageClass自动完成。AccessMode是经常被搞混的一个点ReadWriteOnceRWO表示只能被单个节点挂载ReadWriteManyRWX表示可以被多个节点同时挂载ReadOnlyManyROX表示多个节点只读挂载。这里“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是PGPlacement Group数量具体值要根据OSD数量和数据量来定一般每个OSD建议分配50到100个PG。PG数设太小会导致数据分布不均匀设太大又增加开销。再创建认证用户。Ceph默认启用cephx认证K8s的CSI插件连接Ceph时必须使用有效身份。创建一个专门给K8s用的用户并授权它访问刚才创建的poolceph auth get-or-create client.kubernetes mon allow r osd allow rwx poolkubernetes -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。然后是创建StorageClassapiVersion: 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里真实存在的poolreclaimPolicy设为Delete表示PVC删除时PV和底层RBD块设备一起删除设为Retain则表示保留用于数据归档volumeBindingMode设为WaitForFirstConsumer可以让PV在Pod被调度到节点后再创建这样可以结合节点拓扑避免跨可用区分配块设备。有了StorageClassPVC就可以动态创建了apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mydata spec: accessModes: - ReadWriteOnce storageClassName: ceph-rbd-sc resources: requests: storage: 10Gi创建后kubectl get pvc看状态应该是Boundkubectl 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 namespaceCSI根本找不到。检查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而目标节点没有匹配的labelPod也会卡在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上怼参数问题会少一大半。