<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>持久卷 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E6%8C%81%E4%B9%85%E5%8D%B7/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E6%8C%81%E4%B9%85%E5%8D%B7/index.xml" rel="self" type="application/rss+xml"/><item><title>PVC 为 Pod 提供容器之外的持久存储</title><link>https://www.xbcatm.com/zh/posts/kubernetes-pvc-storage-lifecycle/</link><pubDate>Fri, 13 Aug 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/kubernetes-pvc-storage-lifecycle/</guid><description>持久卷声明绑定后备卷并跨 Pod 重启存续，恢复仍取决于后备存储和拓扑。</description><content:encoded>&lt;p&gt;容器本性易失，所以要用 PersistentVolumeClaim 把真实存储钉到它身上，即使 Pod 被重建也不丢数据。这个拆分是刻意的：PersistentVolume 声明已经存在的容量，PersistentVolumeClaim 请求需要的容量，由控制器把两者配上。有了 StorageClass，声明还能通过 provisioner 自己创建卷，于是没人需要预先造好存储。&lt;/p&gt;
&lt;p&gt;真正会咬人的是：声明与卷各自有独立的生命周期，由各自的规则管着。finalizer 决定声明什么时候才允许消失，回收策略决定卷之后会怎样。删除顺序搞错，就会得到一个卡在 &lt;code&gt;Terminating&lt;/code&gt; 的声明；策略搞错，可能删掉本想保留的数据，也可能留下一份没人在用却仍在计费的存储。&lt;/p&gt;
&lt;h2 id="声明卷与绑定"&gt;声明、卷与绑定&lt;/h2&gt;
&lt;p&gt;绑定是一次「全部满足」而不是「最接近」。一块卷只有在容量不小于请求、访问模式包含所请求的模式、&lt;code&gt;storageClassName&lt;/code&gt; 匹配，并且没有被别的声明占用时，才是候选。除了这几项，&lt;code&gt;volumeMode&lt;/code&gt;（&lt;code&gt;Filesystem&lt;/code&gt; 与 &lt;code&gt;Block&lt;/code&gt;）也必须一致；而已经释放却还没清理的卷并不会变回可用的备件，它的 &lt;code&gt;claimRef&lt;/code&gt; 仍指向上一个声明，所以新的声明根本看不到它。什么都不满足的声明会停在 &lt;code&gt;Pending&lt;/code&gt;，并带一条指明究竟哪一项没满足的事件，这比一句笼统的失败有用得多；常见原因是要的访问模式该类交付不了，以及类名根本不存在。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;accessModes&lt;/code&gt; 没有默认值，必须选一个，而多数单节点块存储 provisioner 实际能交付的就是 &lt;code&gt;ReadWriteOnce&lt;/code&gt;。它同时是单节点的，所以副本分布在不同节点上的 StatefulSet 无法共用一份声明。类支持动态供应时，&lt;code&gt;volumeBindingMode&lt;/code&gt; 决定时机：&lt;code&gt;Immediate&lt;/code&gt; 在声明一出现就创建卷，可能把受拓扑限制的卷放到没有节点能用的可用区；&lt;code&gt;WaitForFirstConsumer&lt;/code&gt; 则让声明一直 &lt;code&gt;Pending&lt;/code&gt;，直到出现消费 Pod，再结合调度约束协调绑定或供应，然后才能完成放置；它不是先完成调度再随意创建卷。&lt;/p&gt;
&lt;h2 id="删除顺序与-finalizer"&gt;删除顺序与 finalizer&lt;/h2&gt;
&lt;p&gt;顺序是 Pod、声明、卷，保护机制由 finalizer 实现。在 Pod 仍挂载着声明时删除声明，什么都不会被删掉：声明拿到删除时间戳、翻成 &lt;code&gt;Terminating&lt;/code&gt;，然后停在那里，因为 &lt;code&gt;kubernetes.io/pvc-protection&lt;/code&gt; 仍在它的 finalizer 列表里。只有再没有任何 Pod 引用这份声明时，控制器才会摘掉那个 finalizer，所以那个「永远 Terminating」的 PVC 可能仍被 Pod 引用，但控制器延迟和其他 finalizer 也需要调查，不能只看状态就排除控制面问题。&lt;/p&gt;
&lt;p&gt;反方向也有同样的保护。&lt;code&gt;PersistentVolume&lt;/code&gt; 带着 &lt;code&gt;kubernetes.io/pv-protection&lt;/code&gt;，所以删除一块已绑定到声明的卷会被推迟到它不再被绑定为止。这个顺序只是延迟误删除，并不代表可以撤销已经设置的 deletionTimestamp，也解释了为什么只要声明还在，卷看起来就会无视删除命令。&lt;/p&gt;
&lt;h2 id="回收策略delete-还是-retain"&gt;回收策略：Delete 还是 Retain&lt;/h2&gt;
&lt;p&gt;声明最终消失后，卷被释放，回收策略接手。&lt;code&gt;Delete&lt;/code&gt; 会删掉 PV 对象以及外部基础设施上的存储资产。&lt;code&gt;Retain&lt;/code&gt; 保留 PV 对象、把它标成 &lt;code&gt;Released&lt;/code&gt;，并且刻意不让它被新的声明使用，因为上一个占用者的数据还在上面——回收于是变成一串手工动作：确认归属与备份，再按存储提供方流程有意回收、复用或删除资产，而不是把删除写成唯一结局。&lt;/p&gt;
&lt;p&gt;动态供应的卷会继承其 StorageClass 的策略，而后者默认是 &lt;code&gt;Delete&lt;/code&gt;。这个默认是否适合数据库，取决于保留和恢复要求，而且它容易被忽略：PVC 与 Pod 清单里都没有一个字提到策略，决定是几个月前在一个集群级对象里做出的，审阅工作负载的人从来看不到。所以在评审一份有状态工作负载时，回收策略应当和容量、访问模式放在一起看。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;最经典的是删不掉的声明。有人删了 PVC，看到 &lt;code&gt;Terminating&lt;/code&gt;，然后开始等——而某个 Pod 仍然挂着它，通常是没缩容到零的 StatefulSet，或者卡在不可达节点上的 Pod。没有报错，没有超时，而这份 PVC 一直占着背后的存储。正确做法是找到那个 Pod，而不是手工删 finalizer；绕过保护可能让仍被工作负载使用的存储过早进入删除流程，并非每个提供程序都会立即执行相同的分离动作。&lt;/p&gt;
&lt;p&gt;第二种是 Retain 留下的孤儿。声明没了，Pod 没了，命名空间可能也没了，一块 &lt;code&gt;Released&lt;/code&gt; 的 PV 和它的存储资产仍等待有意回收。保留可能本来就符合要求；如果没有资产盘点告警，就容易被遗漏，任何工作负载的清单里也看不到它。反向的错误同样安静：某个类的默认 &lt;code&gt;Delete&lt;/code&gt; 策略从未被覆盖，等到有人想找回数据时才发现回收早已把它删掉了。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;同时看绑定的两侧，以及是谁占着这份声明：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get pvc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CLASS:.spec.storageClassName
kubectl get pv -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,POLICY:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name
kubectl get pvc data -o jsonpath=&amp;#39;{.metadata.finalizers}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get pods -A -o json | jq -r &amp;#39;.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName==&amp;#34;data&amp;#34;) | &amp;#34;\(.metadata.namespace)/\(.metadata.name)&amp;#34;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这些保留的诊断本轮没有执行。保护标记说明清理待完成，不证明控制器健康，也不证明一定还有 Pod。第四条查找所有命名空间内名为 data 的声明，因此必须先核对目标命名空间，再确认某个 Pod 是否是消费者。Released 加 Retain 需要归属与恢复决策，不代表应自动删除；旧 claimRef 也可能仍然存在。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/"&gt;Persistent Volumes&lt;/a&gt;，绑定、访问模式、回收策略、&lt;code&gt;Released&lt;/code&gt; 状态与 pvc/pv 保护 finalizer，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/storage/storage-classes/"&gt;Storage Classes&lt;/a&gt;，动态供应、&lt;code&gt;reclaimPolicy&lt;/code&gt; 与 &lt;code&gt;volumeBindingMode&lt;/code&gt;，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>