ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Kubernetes资源迁移:从KRM到PVC/PV的完整避坑指南

Kubernetes资源迁移:从KRM到PVC/PV的完整避坑指南 1. KRM是什么理解Kubernetes资源模型迁移才有章法1.1 先讲一次让我印象深刻的迁移事故去年年中我帮一个客户做集群迁移。旧集群是某个云厂商托管的Kubernetes版本新集群是自建的版本从1.22跨到1.26。项目看起来不复杂应用二十多个命名空间七八个数据库走的是外部RDS按理说把Deployment、Service、ConfigMap导过去就行。结果呢导过去之后有一半工作负载的PVC处于Pending状态两个StatefulSet的Pod反复CrashLoopBackOff还有一个应用的Secret内容跟原集群完全对不上。最离谱的是有个Deployment在新集群里能跑但Pod调度到一个特定节点后容器启动直接报设备不存在——后来查半天才发现那个应用依赖GPU而新集群的Device Plugin根本没装。整个过程折腾了将近一周最后靠着一份手工维护的迁移清单才把问题逐一收掉。这次经历让我意识到一个问题很多人做Kubernetes资源迁移第一反应是把YAML拷过去apply一下但迁移这件事的本质是对Kubernetes资源模型Kubernetes Resource Model简称KRM的理解深度。你有多懂资源对象之间的关联、依赖和生命周期你的迁移就有多稳。1.2 KRM的底层逻辑一切皆对象对象皆可声明KRM不是一个具体工具而是Kubernetes最核心的设计思想。在Kubernetes里无论是一个Pod、一个Service、一个ConfigMap还是一个自定义资源CRD的实例本质上都是API Server里的一个资源对象。每个对象都有几个固定组成部分apiVersion和kind决定了这个对象属于哪个API组、哪种资源类型。metadata名称、命名空间、标签、注解、UID、resourceVersion还有ownerReferences。spec用户声明的期望状态这是迁移时需要重点保留的部分。status系统回写的实际状态通常由各个Controller维护迁移时一般要剥掉不能照搬。KRM的价值在于声明式你告诉集群我要什么而不是你要怎么做。Controller会不断调谐把实际状态拉向期望状态。这个特性决定了迁移的本质把期望状态spec完整搬到目标集群剩下的调谐工作交给新集群的Controller去完成。理解这一点你就能明白为什么有些坑是必然踩的。比如你直接把带status的YAML导过去新集群的Controller会跟你打架比如你把旧的resourceVersion也带过去了API Server直接拒绝比如你强行指定一个在新集群不存在的StorageClassPVC就会永远Pending。1.3 资源迁移到底迁的是什么把KRM想清楚之后迁移这件事就可以拆成三个层面第一层是元数据与声明配置。Deployment、Service、ConfigMap、Secret、Ingress、NetworkPolicy、RBAC这些对象描述的是应用长什么样、怎么被访问、谁有权限操作。这一层迁移相对简单大部分情况下kubectl导出再导入就能搞定。第二层是状态数据。PVC、PV背后的存储数据、数据库里的记录、Redis里的缓存。这一层往往被忽略但它才是迁移中最容易出问题的部分。配置丢了可以重建数据丢了就是事故。第三层是集群能力。StorageClass、CRD、Device Plugin、Webhook、控制器。这些是集群层面的基础设施能力如果不先迁过去上面的应用对象迁过来也没有对应的处理器。比如CRD没装你的自定义资源apply进去只会报no matches for kindDevice Plugin没装申请GPU资源的Pod永远调度不上去。我后来总结了一句话迁移不是一个动作而是一次对集群全貌的体检。你迁移过程中遇到的每一个错误几乎都是在告诉你——某个资源对象的关联关系没处理好。2. 迁移前的资源盘点先搞清楚哪些对象该迁、哪些不该迁2.1 命名空间级资源与集群级资源两条完全不同的迁移路线做迁移盘点时我习惯先把资源按作用域分成两类。这个分类直接决定了导出方式和目标集群的准备动作。命名空间级资源绑定在某个namespace下包括Pod、Deployment、Service、ConfigMap、Secret、PVC、Ingress、NetworkPolicy、ServiceAccount、RoleBinding等。这类资源迁移的难点在于依赖关系Service引用SelectorDeployment引用ConfigMap和SecretPVC引用StorageClassPod引用ServiceAccount。你得先把被引用的对象迁过去再迁引用者。集群级资源不归属于任何namespace包括Namespace本身、Node、PV、StorageClass、ClusterRole、ClusterRoleBinding、CRD、ValidatingWebhookConfiguration等。这类资源通常需要cluster-admin权限才能导出而且很多对象在新集群里已经有默认版本直接覆盖可能引发冲突。我见过不少人在迁移时直接跑一条kubectl get deploy,svc,cm,secret -n myapp -o yaml all.yaml然后把all.yaml原封不动apply到新集群。这个做法风险极大原因有三一个文件里混着多种资源apply顺序完全依赖kubectl的随机排列而依赖关系根本没保证。Deployment导出时会带上status和metadata里的大量系统字段如uid、resourceVersion、creationTimestamp、managedFields这些字段在新集群是无意义的甚至会导致apply失败。集群级和命名空间级混在一起权限模型也容易出问题。正确做法是先盘点后导出再分门别类处理。我通常用下面这张表来做盘点资源类型作用域迁移优先级迁移方式Namespace集群最优先先创建再迁其中资源StorageClass集群最优先先迁移或先确认等价类CRD集群最优先版本一致时迁移否则升级ServiceAccount/RBAC命名空间高随应用一起迁移ConfigMap/Secret命名空间高随应用一起迁移Deployment/StatefulSet命名空间高清洗后迁移PVC命名空间中需要结合存储方案处理HPA/PDB命名空间中随应用迁移确认指标来源Ingress/NetworkPolicy命名空间中涉及DNS与安全策略单独计划2.2 自定义资源KRM里最需要敬畏的一类对象企业级Kubernetes环境里真正让迁移变复杂的往往是自定义资源。CRD本身是一个集群级资源而CRCustom Resource自定义资源实例通常部署在特定命名空间里。这里有一个很常见的误区很多人只备份了CR实例忘了备份CRD。比如你用Prometheus Operator管理监控集群里有很多ServiceMonitor和PrometheusRule实例但你迁移时只把Prometheus Operator的Helm Chart装好没有单独处理旧的CRD版本和CR实例。结果就是Operator起来之后发现旧的CRD结构里有几个新版本已经移除的字段直接调谐失败。我的建议是CRD的迁移要遵循三个原则先迁CRD再迁CR实例。顺序反了的话CR实例apply时找不到对应的kind定义。CRD版本要做diff。新旧集群的CRD版本如果不同你需要确认APIVersion的转换路径。如果CRD定义了多个version并且设置了storage版本迁移后要确保写入的版本和存储版本一致。CR实例里的status字段要删干净。大部分Controller会管理CR的status你从旧集群导出的CR会带着一堆状态信息。直接导入相当于把别人家的状态塞给新集群很多Controller会因此产生误判。我在项目里会专门写一个脚本用jq把CR实例的status和managedFields剥掉只保留spec。这个习惯帮我省掉了大量莫名其妙的Controller报错。2.3 配置类资源与状态类资源迁移策略完全不同配置类资源包括ConfigMap、Secret、Deployment的环境变量、启动参数等。这类资源迁移的核心是可重复——迁错了可以删掉重来不影响数据。状态类资源则完全不同核心代表就是PVC及其背后的PV和存储数据。这类资源迁移失败的代价很高因为数据丢了就真没了。还有一个容易被忽视的类别有状态工作负载的标识性资源。比如StatefulSet创建的PVC命名规则是pvc-name-statefulset-name-序号Pod名称带固定序号。这类资源的身份由名字决定迁移时如果改了命名规则存储卷的挂载关系就会错乱。还有一个更隐蔽的坑Secret的类型和引用关系。Kubernetes里Secret有kubernetes.io/dockerconfigjson、kubernetes.io/tls、Opaque等类型。如果你用kubectl get secret -o yaml导出很多系统自动创建的Secret比如每个namespace自动生成的default-token也会被带出来。这些Secret跟ServiceAccount绑定token内容在新集群是无效的。迁移时应主动过滤只迁业务实际引用的Secret并在迁移后让新集群重新生成ServiceAccount token。3. 完整迁移流程从导出、清洗到应用3.1 导出阶段不要只盯着kubectl getkubectl get是最常用的导出方式但直接-o yaml导出的内容带着一堆系统字段。老版本里有个--export参数可以剥掉状态信息但Kubernetes 1.18之后这个参数就被废弃了所以我现在更推荐用-o json配合jq做精确提取。下面是我在项目中稳定使用的导出方式。以命名空间myapp为例# 1. 导出命名空间下各类资源到独立文件 for kind in configmap secret service deployment statefulset ingress networkpolicy serviceaccount; do kubectl get $kind -n myapp -o json | jq .items[] | {apiVersion, kind, metadata: {name, namespace, labels, annotations}, spec} ${kind}-myapp.json done # 2. 导出集群级资源 kubectl get storageclass -o json storageclass.json kubectl get crd -o json crd.json这里有几个细节值得说明jq提取时我只保留apiVersion、kind、metadata里的name/namespace/labels/annotations和spec。labels和annotations要酌情保留——有些annotation是系统自动生成的如kubectl.kubernetes.io/last-applied-configuration、deployment.kubernetes.io/revision这些应该删掉但业务自定义的annotation如链路追踪的标签、备份工具的标记要留。导出Service时ClusterIP、LoadBalancer IP这类由集群分配的字段会被我主动清掉。原因很简单新集群的IP段跟旧集群不一样你带过去也没用还会干扰Service的创建。导出Deployment时selector和template里的labels必须保持一致这是Kubernetes的强制校验很多人在手工修改YAML时把这里改坏了。3.2 清洗阶段把旧集群的痕迹彻底抹掉清洗是迁移中最容易被低估的一步。很多人觉得导出后apply就行实际上导出的YAML里藏着一堆旧世界残留不清洗必出问题。需要重点清理的字段包括metadata下的系统字段uid、resourceVersion、creationTimestamp、generation、managedFields、selfLink老版本。这些字段在新集群要么没有意义要么会导致冲突。status整体删除包括Deployment的status、Pod的status、PVC的status等。status是Controller写出来的新集群的Controller会自己重新生成。spec中的集群分配字段Service的clusterIP、NodePort、loadBalancerIPPod的nodeName、hostIP。这些是集群运行时分配的迁移时要清空。PVC里的volumeName和storageClass字段要特别处理。volumeName指向旧集群的PV对象直接带过去会导致PVC绑定到一个不存在的PV上。正确做法是先确认新集群的存储方案再决定是保留storageClassName让新集群动态供给还是手动创建PV后通过volumeName绑定。清洗这一步我强烈建议用kubectl自带的dry-run机制做预检kubectl apply --dry-runserver -f cleaned/ -n myapp--dry-runserver会把这批资源真的发给API Server做校验但不会持久化。相比--dry-runclient前者能发现更多服务端校验类的问题比如CRD版本不匹配、字段校验不过等。3.3 应用阶段顺序、校验与回滚缺一不可迁移资源的应用顺序我始终坚持先底座、后业务的原则先创建Namespace和集群级资源Namespace、StorageClass、CRD。再创建无依赖的配置类资源ConfigMap、Secret、ServiceAccount、RBAC。然后创建工作负载Deployment、StatefulSet、DaemonSet。最后创建接入层资源Service、Ingress、NetworkPolicy、HPA。这个顺序背后的逻辑是依赖倒置让每个资源apply时它所依赖的资源已经存在避免Controller反复报错和重试。每批资源apply之前我建议先跑一遍diffkubectl diff -f cleaned/deployment.yaml -n myappkubectl diff会计算本地文件与目标集群现有资源的差异。如果是全新迁移diff会显示全部新增如果是在已有集群上补充资源diff能帮你确认不会误覆盖现有的配置。回滚方案也必须在迁移前准备好。我的习惯是分批次迁移、分批次验证。每迁完一批核心业务就观察10到15分钟重点看Pod是否Ready、日志是否正常、下游调用是否报错。如果出现问题直接删除刚apply的资源回滚到迁移前状态而不是在问题状态下继续往后推。4. 状态数据迁移PVC、PV与存储层的处理是分水岭4.1 存储迁移先想清楚这四件事再说配置迁移做得再好只要涉及有状态服务存储就是分水岭。我见过太多人在这一环节翻车所以建议先想清楚四件事第一件新旧集群的存储类型是否一致。旧集群用的是云厂商的云盘StorageClass新集群是自建的NFS或Ceph这种跨存储类型迁移PVC没法直接复用必须走数据拷贝。第二件PVC的绑定模式是否一致。StorageClass的volumeBindingMode有两种Immediate和WaitForFirstConsumer。Immediate模式下PVC创建即绑定PVWaitForFirstConsumer模式下要等Pod调度到节点后才开始绑定。如果新旧集群的绑定模式不一致迁移后Pod的调度行为会有明显差异。第三件存储数据的拷贝方案。常见方案包括用Velero做备份恢复、在旧集群里起Job把数据推到对象存储、或者直接用kubectl cp在小数据量场景下拷贝。没有存储厂商支持的前提下PV里的数据不会跟着资源对象自动迁移。第四件数据一致性如何保证。如果业务服务在迁移期间还在写入数据先停写再拷贝否则拷出来的数据是不一致的。我在项目里通常会在迁移窗口内把服务副本缩到0数据拷贝完成、新集群验证通过后再逐步扩容。4.2 StatefulSet迁移名字、身份和存储卷挂载的三角关系StatefulSet的迁移比Deployment复杂得多核心原因在于它把Pod的身份和存储卷绑定在了一起。每个StatefulSet管理的Pod名称是statefulset-name-序号对应的PVC名称是volumeClaimTemplate-name-statefulset-name-序号。这个命名规则是固定的迁移时不能改。如果老集群的StatefulSet是myapp-0到myapp-2三个Pod每个Pod挂一块数据盘。迁移到新集群后新Pod重建的顺序也是myapp-0、myapp-1、myapp-2但新PVC是全新的数据是空的必须把旧数据同步到对应的新PVC里。我在项目里常用的一种做法是在旧集群把StatefulSet副本数缩到0确保数据不再变化。用Velero或者手动方式把PVC数据备份到对象存储。在新集群按相同命名规则创建StatefulSet等PVC创建完成后启动恢复Job把数据拉回来。验证数据完整性后把副本数扩容到预期值。这套流程看着简单但有两个细节容易被忽略一是PVC必须跟StatefulSet的模板命名严格一致否则挂载不上二是如果StatefulSet的volumeClaimTemplate里有storageClassName迁移前必须确认新集群存在同名的StorageClass否则PVC会一直Pending。4.3 设备插件资源GPU这类特殊资源迁移前的检查清单这个坑我踩过所以单独拿出来说。Kubernetes里像GPU这种特殊硬件是通过Device Plugin机制暴露给调度器的。应用在Pod里声明nvidia.com/gpu: 1这样的资源申请调度器才会把Pod调度到有GPU的节点上。如果你从旧集群导出一个申请了GPU的Deployment直接apply到新集群如果新集群没有安装对应的Device Plugin会出现两种表现一种是调度器无法识别该资源Pod一直Pending事件里报0/3 nodes are available: 3 Insufficient nvidia.com/gpu另一种是Pod被调度到节点上但容器里访问不到GPU设备。迁移前必须做一次集群能力审计重点检查新集群是否安装了对齐的Device Plugin组件。节点上的GPU驱动版本是否满足容器运行时要求。如果用了NVIDIA的GPU Operator确认它的版本和CRD是否已经部署。顺便说一句这个检查不只针对GPU。任何依赖特殊资源的应用比如FPGA、SR-IOV网卡、持久化内存迁移前都要确认新集群的设备插件是否就绪。资源对象可以一键迁移但硬件能力和设备插件不会跟着YAML走。5. 迁移中的常见坑与完整排查链路5.1 Finalizer引发的删除卡死一次典型的排查过程迁移中途回滚时我们经常需要删除已经apply的资源。有次我删一个自定义资源实例命令执行后一直卡在Terminating状态怎么等都删不掉。排查链路是这样的先看资源详情kubectl get crdinstance myapp-cr -n myapp -o yaml | grep -A 10 finalizers结果发现finalizers字段里有一个custom-cleanup-controller这是部署在旧集群的一个自定义控制器注册的清理钩子。新集群没有这个控制器所以finalizer永远不会被执行完资源就一直停在Terminating。这类问题的修复方式有两种。如果确定这个finalizer对应的清理逻辑在新集群不需要可以直接编辑资源把finalizers字段置空kubectl patch crdinstance myapp-cr -n myapp -p {metadata:{finalizers:[]}} --typemerge如果这个finalizer对应的业务逻辑是必要的比如删除前要清理外部依赖则必须先在新集群部署对应的控制器再执行删除。这个经历给我一个教训迁移前导出资源时应该把metadata.finalizers字段作为重点审查对象。任何带有finalizer的资源都要弄清楚是谁注册的、在新集群里谁能执行这个清理动作。否则一旦需要回滚资源就可能卡在Terminating拖慢整个迁移进度。5.2 ServiceAccount的token Secret明明导入了却报鉴权失败我还遇到过一个问题某个应用在新集群启动后调用Kubernetes API一直报Unauthorized。排查了很久发现Pod挂载的ServiceAccount对应的Secret token在迁移时被我原样拷过去了但这个token是旧集群签发的新集群的TokenReview根本不认。Kubernetes 1.24之后ServiceAccount的Secret token不再是自动创建的那一套逻辑而是通过TokenRequest API动态签发并且有有效期。迁移时如果你手动导出了ServiceAccount关联的Secret反而会把旧token带进新集群导致一系列鉴权问题。正确做法是只迁移ServiceAccount对象本身删除关联的Secret让新集群重新生成token。如果你的应用需要长时间运行的token比如通过API访问集群的外部系统迁移后应该用新集群的TokenRequest重新签发并配置好正确的audience和过期时间。5.3 集群版本差异导致的服务端默认值变化Kubernetes的几个小版本之间API对象的默认值经常有变化。比较典型的例子是Deployment的revisionHistoryLimit、Pod的restartPolicy、Service的sessionAffinity。这些字段你在导出时可能没写但旧集群的API Server在存储时会用当时的默认值把字段补全。迁移到一个新版本集群后有两个层面的风险一是你导出的YAML里带了旧版本的默认值而新版本对这个字段的校验更严格导致apply失败。比如某些版本对PodSecurityContext的seccompProfile有强制要求旧版本可能默认是Unconfined新版本会要求显式声明。二是你的YAML里没带这个字段新版本用了不同的默认值导致行为变化。这类问题最隐蔽因为不会报错但运行行为跟预期不一样。我的习惯是迁移前拿新旧集群的版本做一次kubectl api-resources和kubectl explain对比重点关注自己所用资源类型的spec字段在新版本中的默认值变化。另外一个实用技巧是迁移完成后把关键Deployment的完整YAML导出来跟旧集群做一次diff逐字段确认差异是否符合预期。5.4 资源配额和LimitRange应用能跑但调度不过去还有一个容易忽略的点是ResourceQuota和LimitRange。这两个资源本身也是KRM中的对象但它们的作用是约束同命名空间下其他资源的创建。迁移时如果先把ResourceQuota导过去再导Deployment可能Deployment直接创建失败报exceeded quota。如果先导Deployment再导ResourceQuota倒是能创建成功但后续扩容会被限制。我的处理方式是这两类资源单独批次迁移且在批次顺序上放在工作负载之后。先让工作负载创建成功并完成验证再导入ResourceQuota和LimitRange最后做一次完整的容量验收。5.5 安全类问题未授权访问的排查要在迁移后立刻做这个话题跟迁移本身关系密切。迁移完成后新集群的网络策略、认证鉴权可能还没完全配好如果新集群的入口暴露在公网很容易出现未授权访问的问题。我的做法是在迁移后的第一时间做一轮安全自查确认apiserver的匿名访问是否关闭--anonymous-authfalse。确认所有Service的externalIPs、NodePort没有暴露不该暴露的端口。确认NetworkPolicy是否已经覆盖核心业务命名空间。检查RBAC绑定尤其是cluster-admin角色有没有被误绑定给普通ServiceAccount或用户。迁移是个很好的安全梳理时机。旧集群可能有历史遗留的过度授权既然资源都搬到新环境了不如趁机把权限模型理清楚这也是我做迁移项目时额外给客户提供的价值点。6. 几次实战后的心得工具链、校验与团队协作6.1 项目中验证过的工具链组合有人问过我迁移用Velero不就行了为什么还要手动处理KRM资源我的回答是Velero是很好的备份恢复工具它的核心能力是备份和恢复这个动作本身但企业级的迁移往往伴随着集群版本升级、存储类型切换、网络架构调整这些场景下你需要对导出的YAML做大量定制化修改。只靠Velero达不到这个灵活度。我个人在项目中常用的组合是工具用途使用场景kubectl jq资源导出与清洗大部分配置类资源Velero备份恢复大规模、多命名空间、含PV数据的整体迁移krew插件如konvert资源格式转换跨版本迁移时调整API版本Kustomize资源定制化按环境生成差异化的迁移清单Argo CD或其他GitOps工具持续同步迁移后的配置漂移控制这套组合的核心理念是把导出、清洗、定制、应用四个阶段拆开每个阶段用最合适的工具而不是指望一个工具包打天下。6.2 迁移验收清单别让看起来正常骗了你应用能跑起来不代表迁移成功了。我每次迁移后都会按下面的清单做验收Pod全部Running且Ready查看日志确认没有报错。Service的Endpoint是否指向了正确Pod的IP。ConfigMap和Secret的内容是否与旧集群一致用diff验证。PVC状态为Bound且存储卷可读写写入测试文件后能读到。HPA能否正常拉取指标并执行扩缩容。Ingress的DNS解析和HTTPS证书访问正常。从旧集群保留的测试流量验证业务完整性比如登录流程、核心接口调用。检查事件流确认没有持续报错的异常事件。这套清单是我从Pod能跑但业务挂了的教训里总结出来的。有的问题要等业务流量进来才暴露所以迁移完成后的验证阶段最好是找业务负责人一起做而不是光看技术指标。6.3 迁移过程中最容易忽视的协作问题最后说点技术之外的事。Kubernetes资源迁移看起来是纯技术活但真正决定项目成败的往往是协作。一个典型场景应用团队告诉你这个服务没有状态让你放心迁。结果迁完发现应用把数据写在了容器本地临时目录里一问才知道他们的应用本来有个配置项可以切换存储后端但因为测试环境用的是临时目录没人注意到生产环境的数据是落在PVC上的。另一个场景迁移窗口需要停服但业务方没有提前跟客户沟通结果迁移那几天正好赶上业务高峰期数据一致性根本无法保证。我的经验是在迁移正式执行前必须做一次跨团队的迁移评审会明确三件事每个应用的存储依赖、迁移期间是否允许停服、回滚预案的触发条件。这些信息比任何技术参数都重要。写在最后的一点体会做了几年Kubernetes相关的项目我越来越觉得资源迁移这件事本质上是对整个Kubernetes资源模型理解的一次大考。你平时觉得kubectl apply一下就行的简单操作在迁移这个场景下会被放大成无数个细节问题字段是否被清理、依赖是否有序、状态是否剥离、存储是否可达。每一个细节背后都是一次生产事故的可能性。如果你接下来也要做类似的迁移项目我的建议是不要急着动手导资源先花两天时间把新旧集群的资源清单盘一遍把上面说的那几个坑对照检查一遍。准备做得越充分迁移当天就越轻松。这些经验是实打实踩坑踩出来的希望能帮你少走一些弯路。
返回列表