无状态的 Pod 一般通过 Deployment 创建,而有状态的服务通过 StatefulSet 创建,例如数据库就是典型的有状态服务,本文通过部署 Redis 来测试验证如何手动、自动创建持久卷,并了解相关概念。
基础概念
PersistentVolumeClaim(PVC)持久卷声明
PVC 是一种资源对象,用于申请存储,声明需要的存储容量和访问模式等。
一般由开发者创建,系统会自动将 PVC 绑定到 PV,而后在 Pod 中指定 PVC 即可挂载使用存储。
如果是通过 StatefulSet 创建 Pod,StatefulSet 提供了 volumeClaimTemplates 字段,用于为 StatefulSet 中的每个 Pod 动态生成 PVC,跟手动创建 PVC 效果相同。
PersistentVolume(PV)持久卷
PVC 声明所需要的存储,由 PV 提供实际的存储。
PV 通过 storageClassName 声明所属的存储类,PVC 通过指定 storageClassName 选择匹配的 PV。
举个例子:管理员分别创建了 1GB、3GB、5GB 大小的 PV,并且定义这些 PV 的 storageClassName 为 “local-storage”(多个 PV 的类名可以是相同的),在 PVC 中指定 storageClassName 为 “local-storage”,当 PVC 中声明需用 2GB 存储时,系统会自动分配 3GB 的 PV 给 PVC 使用,这样我们无需关注具体的存储名,只需要声明我们对存储的要求即可。
PersistentVolume Provisioner 持久卷提供者
初步了解了 PV 和 PVC,我们知晓每个 Pod 所使用的存储通过 PVC 声明,PCV 自动绑定到 PV,如果每个 Pod 所需的 PV 都手动创建,这肯定是个枯燥和容易出错的步骤。
PersistentVolume Provisioner 插件可以解决这个问题,它可以根据 StorageClass 动态创建 PersistentVolume,实现自动创建 PV。
后文会安装 local-path-provisioner,并且创建 Pod 进行测试。
StorageClass 存储类
PVC 通过 StorageClass 来绑定到具体的 PV(更具体的说:PVC 通过指定 storageClassName 选择使用的 StorageClass,而 StorageClass 定义了供应机制和配置决定了最终为 PVC 配备的具体 PV 资源,实现了声明式的动态供应。)
StorageClass 好比于存储的分类,可以有不同维度的分组
如磁盘速度(standard - 普通本地存储、ssd - 固态硬盘存储、high-speed - 高速存储)
也可以根据环境(dev - 开发和测试环境存储、prod - 生产环境存储)
也可以根据供应商(google-disk - GCP磁盘、aws-ebs - AWS弹性块存储)等
手动创建 PersistentVolume(PV)
手动创建基于本次磁盘的 PV
pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv
spec:
storageClassName: local-storage
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
local:
path: /redis-data
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-01
metadata.name 定义了名为 “local-pv” 的 PV,它提供了 5GB 存储空间,存储使用的是本地磁盘的 “/redis-data” 路径。
下方会有例子创建 StorageClass 并使用 provisioner 自动创建 PV 的示例。
创建基于本地存储的 PV 时,可以通过配置中的 nodeAffinity 节点亲和性配置指定 PV 在哪些符合条件的节点创建,以上配置定义 的 PV 会被创建在名为 “worker-01” 节点。
执行
$ kubectl apply -f pv.yaml
查看
$ kubectl get pv
输出
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
local-pv 5Gi RWO Retain Available local-storage 2m2s
当存储被使用后,状态会改变。
删除
$ kubectl delete pv local-pv
启动 StatefulSet Redis 实例
sts.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:6
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: redis-data
mountPath: /redis-data
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "local-storage"
resources:
requests:
storage: 1Gi
以上 PVC 配置声明使用 "local-storage" 存储类提供存储,需要 1GB 大小的存储空间,并且访问模式是单实例读写访问。
此处通过 volumeClaimTemplates 字段声明所需存储,其效果等同于先创建 PVC,而后在 StatefulSet 指定 PVC。
执行
$ kubectl apply -f sts.yaml
查看
$ kubectl get statefulsets
删除
$ kubectl delete statefulsets redis
Redis 启动后,可以通过命令查看 Pod 节点的 IP,而后通过 redis-cli 连接到服务
$ root@mater:~# redis-cli -h 172.11.22.33
172.11.22.33:6379> ping
PONG
172.11.22.33:6379>
写入一些数据后,Redis 存盘时会将 dump.rdb 文件存储在 worker-01 机器的 /redis-data 目录下,也可以手动写入一些数据到容器的 /data 目录进行验证
$ kubectl exec redis-statefulset-0 -- /bin/sh -c "echo 'test-data' > /data/test.txt"
在 worker-01 节点查看
root@worker-01:~# ls /redis-data/
dump.rdb test.txt
这样 StatefulSet 类型的 Redis 就部署好了,能通过 cli 访问并且存储到了 PV 符合我们的预期,接下来通过 Provisioner 来自动创建 PV
安装 Local Path Provisioner
集群中一般默认没有 Provisioner,这里安装使用的是 Rancher 提供的 Local Path Provisioner
安装
$ kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.24/deploy/local-path-storage.yaml
可以打开 Local Path Provisioner 的仓库找最新的 Yaml 地址
输出
namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
configmap/local-path-config created
通过命令 kubectl get pods -n local-path-storage
可以查询到 provisioner 对应的 Deployment Pod
root@master:~/# kubectl get pods -n local-path-storage
NAME READY STATUS RESTARTS AGE
local-path-provisioner-8559f79bcf-2mpgv 1/1 Running 0 109s
通过命令 kubectl get storageclass
查看 StorageClass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 1h
查看日志
$ kubectl -n local-path-storage logs -f -l app=local-path-provisioner
验证 Local Path Provisioner
创建 PVC,并由 Pod 使用
$ kubectl create -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pvc/pvc.yaml
$ kubectl create -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pod/pod.yaml
这里是手动创建的 PVC(在文章上半部分,我们创建的 StatefulSet Redis 是通过 volumeClaimTemplates 字段生成的 PVC),它声明了 128MB 的存储空间,storageClassName 指定的名为 “local-path” 的 StorageClass,由其 Provisioner 分配 PV
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-path-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 128Mi
而后 Pod 的配置如下,Pod 名为 volume-test,通过 persistentVolumeClaim 设置刚创建的 PVC,将由 PVC 通过 StorageClass 获得的 PV 绑定到 /data 目录,留意这里的 volumes 参数,一个 Pod 支持绑定多个 Volume
apiVersion: v1
kind: Pod
metadata:
name: volume-test
spec:
containers:
- name: volume-test
image: nginx:stable-alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- name: volv
mountPath: /data
ports:
- containerPort: 80
volumes:
- name: volv
persistentVolumeClaim:
claimName: local-path-pvc
接下来查看由 Provisioner 自动创建的 PV 和刚创建的 PVC
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-e452f51a-abd7-4d0a-a699-eca3adca8127 128Mi RWO Delete Bound default/local-path-pvc local-path 1h
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
local-path-pvc Bound pvc-e452f51a-abd7-4d0a-a699-eca3adca8127 128Mi RWO local-path 1h
PV 及 PVC 中输出的 STATUS = Bound 表示 PVC 成功找到匹配的 PV 并被绑定。
查看 Pod 并通过 Pod 写入数据到存储
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
volume-test 1/1 Running 0 1h
$ kubectl exec volume-test -- sh -c "echo local-path-test > /data/test"
在 /opt/local-path-provisioner 目录下可以找到生成的文件夹,我这里示例文件夹如下
root@master:~# cat /opt/local-path-provisioner/pvc-e452f51a-abd7-4d0a-a699-eca3adca8127_default_local-path-pvc/test
local-path-test
接下来初步下 provisioner 的工作原理,理解 StorageClass
Provisioner 工作流程
- Provisioner Pod 向 K8s 的 API Server 注册自己支持的 provisioner 类型
- StorageClass 通过 provisioner 字段指定要使用的 provisioner(rancher.io/local-path)
- K8s 的 Persistent Volume Controller,收到 PVC 的创建通知,匹配 provisioner 后通过 RPC 接口下发请求到 Pod
- Provisioner Pod 根据 StorageClass 参数创建对应存储资源作为 PV
- PV 和 PVC 绑定后,应用 Pod 即可使用该 PV 存储卷
我们打开 local-path-storage.yaml 就可以看到其定义的很多类,NameSpace、权限、Deployment、StorageClass 等,接下来摘录部分配置。
先看下 StorageClass 配置,它名为 local-path,指定 provisioner 为 rancher.io/local-path
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
另外 Rancher 提供的 Provisioner 有用到 ConfigMap,可以看到参数通过 ConfigMap 提供,未集成到 Pod 中,这样可以方便的修改配置。
kind: ConfigMap
apiVersion: v1
metadata:
name: local-path-config
namespace: local-path-storage
data:
config.json: |-
{
"nodePathMap":[
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths":["/opt/local-path-provisioner"]
}
]
}
nodePathMap 配置每个节点的存储路径,node 指代节点,此处配置的 DEFAULT_PATH_FOR_NON_LISTED_NODES 表示未配置的节点设置默认路径,paths 指定了 /opt/local-path-provisioner 为存储默认路径,Paths 可以指定多个路径,可以利用多磁盘空间、定义优先级、配置冗余备选,但需要注意同一个 PV 只会使用路径中的一个。
{
"node":"worder-01",
"paths":["/opt/local-path-provisioner", "/data1"]
}
总结
本篇通过部署 StatefulSet Redis 进而了解到了如何手动创建本地存储卷并使用,同时安装 Local Path Provisioner 用于自动创建本地存储卷并做了验证。
存储及 StatefulSet 还有很多知识点及细节可探索,待后续学习。