第10章:K8s 数据持久化
我们已经知道,Pod 的生命周期是短暂的。它们可能因为节点故障、版本更新、或者弹性伸缩而被销毁和重建。如果我们将数据(比如数据库文件)直接存储在 Pod 的文件系统中,那么当 Pod 消失时,数据也会随之而去。
这对于无状态应用 (Stateless Application)(如我们的 my-app Web 服务器)来说没有问题,但对于有状态应用 (Stateful Application)(如数据库、消息队列等)来说是致命的。
为了在 K8s 中管理有状态应用,我们需要一种独立于 Pod生命周期的、可靠的数据持久化机制。K8s 通过一套设计精巧的 API 资源来解决这个问题:PersistentVolume (PV) 和 PersistentVolumeClaim (PVC)。
10.1 PersistentVolume (PV) & PersistentVolumeClaim (PVC)
K8s 的存储设计,巧妙地将“存储资源的管理”和“存储资源的使用”这两个角色进行了解耦。
-
PersistentVolume (PV): 持久卷
- 角色:集群管理员 (Administrator)
- 是什么:PV 是集群中的一块网络存储。它可以是云服务商提供的磁盘(如 AWS EBS, GCP Persistent Disk),也可以是本地的存储设备(如 NFS)。
- 类比:你可以把 PV 想象成是管理员在公司仓库里,提前准备好的一批“移动硬盘”。这些硬盘大小不一,性能各异。
-
PersistentVolumeClaim (PVC): 持久卷声明
- 角色:开发者 (Developer) / 应用
- 是什么:PVC 是用户对存储资源的一次“申请”。它描述了“我需要多大的硬盘(比如 5GB)”、“我需要什么样的读写性能(比如支持多节点读写)”等需求。
- 类比:PVC 就像是你(开发者)填写的一张“硬盘申领单”。
工作流程 (PV 和 PVC 的绑定过程):
- 管理员创建一批 PV,放入集群的“资源池”中。
- 开发者为自己的应用创建一个 PVC,声明需要一块 5GB 的硬盘。
- K8s 的控制平面会像一个仓库管理员,自动在资源池里寻找能够满足这个 PVC 要求的、尚未被使用的 PV。
- 如果找到了一个合适的 PV(比如一块 10GB 的硬盘),K8s 就会将这个 PVC 和 PV 绑定 (Bind) 在一起。
- 一旦绑定,这个 PV 就被这个 PVC “独占”了,其他 PVC 不能再使用它。
- 最后,开发者可以将这个 PVC 像普通 Volume 一样,挂载到自己的 Pod 中使用。
这个设计的最大好处是抽象。作为开发者,你只需要关心如何申请 (PVC),而完全不需要关心底层存储到底是什么、由谁提供。这些复杂的细节都由集群管理员通过 PV 来处理。
10.2 StorageClass:动态供给存储
手动地预先创建一堆 PV 让管理员很累。如果每次开发者提交一个 PVC,K8s 都能自动地根据 PVC 的要求,去调用云平台的 API,动态地创建一个 PV 并与之绑定,那该多好?
StorageClass 就是实现这个“动态供给 (Dynamic Provisioning)”的魔法师。
- 是什么:StorageClass 定义了“如何创建 PV”的模板。它描述了要使用哪种存储插件(比如
aws-ebs),以及创建时需要哪些参数(比如磁盘类型是gp2还是io1)。 - 工作方式:
- 管理员在集群中创建一个或多个 StorageClass。
- 开发者在创建 PVC 时,可以指定要使用哪个 StorageClass。
- K8s 看到这个 PVC 后,就会找到对应的 StorageClass,并根据它的定义,自动地去创建所需的 PV,然后将 PV 和 PVC 绑定。
在大多数现代 K8s 集群(包括 Minikube 和各大云厂商的 K8s 服务)中,都会有一个默认的 StorageClass。这意味着,你甚至不需要手动创建 PV 和 StorageClass,你只需要创建 PVC,K8s 就会为你搞定剩下的一切!
10.3 [实战] 为我们的数据库 Pod 挂载持久化存储
让我们来为我们的 postgres 数据库实现数据持久化。我们将不再使用 Docker 的数据卷,而改用 K8s 的标准方式。
第一步:检查 StorageClass
在我们的 Minikube 集群中,已经有一个默认的 StorageClass 了。我们可以查看一下:
kubectl get sc
# sc 是 storageclass 的缩写
你会看到一个名为 standard (或类似名字) 的 StorageClass。
mac@192 docker-k8s-little-book-source-code % kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
standard (default) k8s.io/minikube-hostpath Delete Immediate false 2d13h
mac@192 docker-k8s-little-book-source-code %
第二步:创建 PVC (持久卷声明)
创建一个 database-pvc.yaml 文件,来申请一块存储空间。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
# 指定存储类别,我们可以省略,因为它会使用默认的
# storageClassName: standard
# 访问模式,定义了 PV 该如何被节点挂载
# ReadWriteOnce (RWO): 卷可以被单个节点以读写方式挂载 (最常用)
# ReadOnlyMany (ROX): 卷可以被多个节点以只读方式挂载
# ReadWriteMany (RWX): 卷可以被多个节点以读写方式挂载
accessModes:
- ReadWriteOnce
# 资源需求
resources:
# 声明需要 1Gi 的存储空间
requests:
storage: 1Gi
应用它:kubectl apply -f database-pvc.yaml
查看 PVC 和自动创建的 PV:
kubectl get pvc postgres-pvc
kubectl get pv
你会看到 PVC 的状态是 Bound,并且 K8s 已经自动为我们创建了一个对应的 PV。
mac@192 docker-k8s-little-book-source-code % kubectl get pvc postgres-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
postgres-pvc Bound pvc-8c250aa3-f669-4a13-848e-a60e26ad67f4 1Gi RWO standard <unset> 5h26m
mac@192 docker-k8s-little-book-source-code % kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-68eb8057-18bf-4164-9730-b6a874e2e916 1Gi RWO Delete Bound default/postgres-storage-postgres-db-0 standard <unset> 16m
pvc-8c250aa3-f669-4a13-848e-a60e26ad67f4 1Gi RWO Delete Bound default/postgres-pvc standard <unset> 5h26m
第三步:创建 StatefulSet (有状态应用部署)
对于有状态应用,我们通常不使用 Deployment,而是使用一个更适合的控制器:StatefulSet。StatefulSet 提供了 Deployment 的所有功能,此外还为 Pod 提供了稳定的、唯一的网络标识符和稳定的、持久的存储。
创建一个 database-statefulset.yaml 文件:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-db
spec:
serviceName: "postgres"
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
value: mysecretpassword
# 将 PVC 挂载到容器的
# PostgreSQL 数据目录 /var/lib/postgresql/data
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
# 卷声明模板,StatefulSet 会根据这个模板
# 为每个 Pod 副本创建一个对应的 PVC
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
注意:上面的 YAML 结合了 PVC 和 StatefulSet,是更高级的用法(官方推荐,无需单独的 database-pvc.yaml 文件了)。
- ✅ 自动化:StatefulSet 自动创建和管理 PVC
- ✅ 可扩展:每个 Pod 副本都有独立的存储
- ✅ 官方推荐:符合 Kubernetes 设计理念
你也可以先创建独立的 PVC,然后在 StatefulSet 的 volumes 里直接引用它:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-db
spec:
serviceName: "postgres"
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
value: mysecretpassword
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
# 🔑 关键:直接引用前面创建的 PVC
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
- ✅ 可控性强:可以精确控制 PVC 的属性
- ✅ 灵活性高:适合特殊存储需求
- ❌ 需要手动管理:不适合大规模部署
第四步:创建 Service
为了让我们的 Web 应用能访问到数据库,还需要为它创建一个 Service。创建一个 database-service.yaml。
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
ports:
- port: 5432
selector:
app: postgres
第五步:部署并验证
部署 StatefulSet 和 Service:
kubectl apply -f database-statefulset.yaml
kubectl apply -f database-service.yaml
现在,你可以 exec 进入 Pod,创建一些数据。然后,尝试删除这个 Pod:
# 进入 Pod 的容器内部
kubectl exec -it postgres-db-0 -- /bin/bash
# 任意创建一些数据
psql -U postgres -d postgres -c "CREATE TABLE IF NOT EXISTS test (id SERIAL PRIMARY KEY, name TEXT);"
psql -U postgres -d postgres -c "INSERT INTO test (name) VALUES ('K8s 持久化存储');"
# 查看数据
psql -U postgres -d postgres -c "SELECT * FROM test;"
# 列出所有 Pod
kubectl get pods
# 删除 Pod
kubectl delete pod postgres-db-0
StatefulSet 会立刻重建一个新的 postgres-db-0 Pod。当新 Pod 启动后,它会自动重新挂载之前由 PVC 提供的同一块持久化存储。你再次 exec 进去,会发现数据完好无损!
运行结果示例
mac@192 docker-k8s-little-book-source-code % kubectl exec -it postgres-db-0 -- /bin/bash
postgres-db-0:/# psql -U postgres -d postgres -c "CREATE TABLE IF NOT EXISTS test (id SERIAL PRIMARY KEY, name TEXT);"
psql -U postgres -d postgres -c "INSERT INTO test (name) VALUES ('K8s 持久化存储');"
CREATE TABLE
INSERT 0 1
postgres-db-0:/# psql -U postgres -d postgres -c "SELECT * FROM test;"
id | name
----+----------------
1 | K8s 持久化存储
(1 row)
postgres-db-0:/# kubectl get pods
bash: kubectl: command not found
postgres-db-0:/#
exit
command terminated with exit code 127
mac@192 docker-k8s-little-book-source-code % kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-deployment-5f69bcbfcd-k9tcg 1/1 Running 0 26h
my-app-deployment-5f69bcbfcd-m4878 1/1 Running 0 26h
postgres-db-0 1/1 Running 0 8m20s
mac@192 docker-k8s-little-book-source-code % kubectl delete pod postgres-db-0
pod "postgres-db-0" deleted
mac@192 docker-k8s-little-book-source-code % kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-deployment-5f69bcbfcd-k9tcg 1/1 Running 0 26h
my-app-deployment-5f69bcbfcd-m4878 1/1 Running 0 26h
postgres-db-0 1/1 Running 0 13s
mac@192 docker-k8s-little-book-source-code % kubectl exec -it postgres-db-0 -- /bin/bash
postgres-db-0:/# psql -U postgres -d postgres -c "SELECT * FROM test;"
id | name
----+----------------
1 | K8s 持久化存储
(1 row)
10.4 本章小结
你已经攻克了在 K8s 中运行有状态应用的最后一道难关。
- 本章回顾:
- 我们理解了 PV (管理员的硬盘) 和 PVC (开发者的申请单) 之间解耦的设计哲学。
- 我们了解了 StorageClass 是实现动态存储供给的关键。
- 我们学会了如何为有状态应用(如数据库)创建 PVC 来申请持久化存储。
- 我们认识了专门用于部署有状态应用的 StatefulSet 控制器。
- 我们通过实战,成功地为一个
postgres数据库 Pod 挂载了持久化存储,并验证了数据的持久性。
至此,K8s 篇的核心概念已经全部介绍完毕。你已经具备了在 K8s 上部署一个相对完整的、包含无状态和有状态服务的应用所需的所有理论知识。
在最后一章,我们将迎来我们的大结局——将所有学过的知识融会贯通,把我们的主线项目完整地部署到 Kubernetes 上!